flame_ldtk
A Flutter package for integrating LDtk levels into Flame Engine games.
Features
- 🌍 World Management - Simplified API with
LdtkWorldfor managing levels and assets - 🎮 Super Simple Export Support - Optimized loading of LDtk levels using Super Simple Export format
- 🗺️ Level Rendering - Individual layer rendering with transparency support
- 🖼️ Background Images - Automatic background loading with positioning modes
- 🎯 Entity Parsing - Extract entities with positions, sizes, custom fields, colors, and sprites
- 🖼️ Entity Tiles - Automatic sprite loading from entity tiles defined in LDtk
- 🧱 IntGrid Support - CSV-based IntGrid for collisions and game logic
- 🔄 Level Switching - Change levels dynamically without recreating components
- 🎨 Flexible Architecture - Override hooks to customize entity rendering
- 📦 Generic Design - No built-in collision logic, adapt to your game type
- ⚡ Optimized Performance - LRU cache system and fast CSV parsing
📖 Looking for JSON format support? See JSON_FORMAT.md (experimental, not fully implemented)
Installation
Add flame_ldtk to your pubspec.yaml:
dependencies:
flame: ^1.32.0
flame_ldtk: ^0.2.0
LDtk Setup
Super Simple Export
- Create your level in LDtk
- Go to Project Settings → Super Simple Export
- Enable Super Simple Export
- Set your export path (e.g.,
assets/world/simplified/) - Save your project to generate the export files
Each exported level will contain:
_composite.png- Complete level visual (optional, use individual layers instead)[LayerName].png- Individual layer images (e.g.,Tiles.png)data.json- Level metadata and entities (lightweight, ~500B for simple levels)[LayerName].csv- IntGrid layers (for collisions, etc.)
For background images: Keep the .ldtkl file to read background configuration.
Basic Usage
1. Add assets to pubspec.yaml
flutter:
assets:
- assets/world/simplified/Level_0/ # Simplified export folder
- assets/world/Level_0.ldtkl # For background and entity tiles
- assets/world.ldtk # LDtk project file
- assets/background.png # Background image
- assets/tilemap.png # Tileset for entity sprites
2. Load a level in your game
Simple usage with LdtkWorld:
import 'package:flame/game.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
// Load the world once
final world = await LdtkWorld.load('assets/world.ldtk');
// Create and load a level
final level = LdtkLevelComponent(world);
await level.loadLevel('Level_0');
await add(level);
}
}
With collisions and background:
import 'package:flame/game.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
// Load the world - it handles all paths automatically
final world = await LdtkWorld.load('assets/world.ldtk');
// Create level component
final level = LdtkLevelComponent(world);
// Load a level with collision layer
// Background and entity sprites are loaded automatically
await level.loadLevel('Level_0', intGridLayers: ['Collisions']);
await add(level);
}
}
Switching levels dynamically:
class MyGame extends FlameGame {
late LdtkLevelComponent level;
@override
Future<void> onLoad() async {
final world = await LdtkWorld.load('assets/world.ldtk');
level = LdtkLevelComponent(world);
await level.loadLevel('Level_0', intGridLayers: ['Collisions']);
await add(level);
}
Future<void> goToNextLevel() async {
// Change level without recreating the component
await level.loadLevel('Level_1', intGridLayers: ['Collisions']);
}
}
Working with Entities
Customize entity rendering
Override onEntitiesLoaded() to handle your entities:
class MyLevelComponent extends LdtkLevelComponent {
@override
Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
for (final entity in entities) {
switch (entity.identifier) {
case 'Player':
final player = PlayerComponent(entity, levelData!);
await add(player);
break;
case 'Enemy':
final enemy = EnemyComponent(entity, levelData!);
await add(enemy);
break;
case 'Coin':
final coin = CoinComponent(entity);
await add(coin);
break;
}
}
}
}
Create entity components
class PlayerComponent extends PositionComponent {
final LdtkEntity entity;
final LdtkLevel level;
PlayerComponent(this.entity, this.level) {
position = entity.position; // LDtk position
size = entity.size; // Entity size from LDtk
}
@override
Future<void> onLoad() async {
// Render with entity color from LDtk
final color = entity.color ?? Colors.blue;
final rect = RectangleComponent(
size: size,
paint: Paint()..color = color,
);
await add(rect);
}
}
Access custom fields
class ChestComponent extends PositionComponent {
final LdtkEntity entity;
ChestComponent(this.entity) {
position = entity.position;
size = entity.size;
// Access custom fields defined in LDtk
final loot = entity.fields['loot'] as String? ?? 'gold';
final amount = entity.fields['amount'] as int? ?? 10;
print('Chest contains $amount $loot');
}
}
Working with IntGrid (Collisions)
Load IntGrid layers
class MyLevelComponent extends LdtkLevelComponent {
@override
Future<void> onLoad() async {
// Load level with collision layer
await loadLevel(
'assets/world/simplified/Level_0',
intGridLayers: ['Collisions'], // Load IntGrid layers
);
}
}
Implement collision detection
class PlayerComponent extends PositionComponent {
final LdtkLevel level;
Vector2 velocity = Vector2.zero();
@override
void update(double dt) {
final collisions = level.intGrids['Collisions'];
if (collisions == null) return;
// Calculate new position
final newX = position.x + velocity.x * dt;
final newY = position.y + velocity.y * dt;
// Check horizontal collision
if (_canMoveTo(collisions, newX, position.y)) {
position.x = newX;
}
// Check vertical collision
if (_canMoveTo(collisions, position.x, newY)) {
position.y = newY;
}
}
bool _canMoveTo(LdtkIntGrid grid, double x, double y) {
// Check four corners of player hitbox
final corners = [
Vector2(x, y), // Top-left
Vector2(x + size.x, y), // Top-right
Vector2(x, y + size.y), // Bottom-left
Vector2(x + size.x, y + size.y), // Bottom-right
];
for (final corner in corners) {
if (grid.isSolidAtPixel(corner.x, corner.y)) {
return false; // Collision detected
}
}
return true; // Can move
}
}
IntGrid helper methods
final grid = level.intGrids['Collisions']!;
// Check by pixel position
bool solid = grid.isSolidAtPixel(128.5, 64.0);
// Check by grid cell
bool solid = grid.isSolid(16, 8); // Cell coordinates
// Get cell value
int value = grid.getValue(16, 8); // Returns 0 for empty, 1+ for solid
// Grid properties
int cellSize = grid.cellSize; // Size of each cell in pixels
int width = grid.width; // Grid width in cells
int height = grid.height; // Grid height in cells
Complete Platformer Example
import 'package:flame/game.dart';
import 'package:flame/components.dart';
import 'package:flame/input.dart';
import 'package:flutter/services.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class PlatformerGame extends FlameGame with KeyboardEvents {
PlayerComponent? player;
@override
Future<void> onLoad() async {
// Load the world
final world = await LdtkWorld.load('assets/world.ldtk');
// Create and load level
final level = MyLevelComponent(world);
await level.loadLevel('Level_0', intGridLayers: ['Collisions']);
await add(level);
player = level.player;
}
@override
KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keys) {
player?.onKeyEvent(event, keys);
return KeyEventResult.handled;
}
}
class MyLevelComponent extends LdtkLevelComponent {
PlayerComponent? player;
MyLevelComponent(super.world);
@override
Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
for (final entity in entities) {
if (entity.identifier == 'Player') {
player = PlayerComponent(entity, levelData!);
await add(player!);
}
}
}
}
class PlayerComponent extends PositionComponent {
final LdtkEntity entity;
final LdtkLevel level;
// Physics
static const double moveSpeed = 100.0;
static const double jumpForce = -300.0;
static const double gravity = 800.0;
Vector2 velocity = Vector2.zero();
bool isOnGround = false;
bool isMovingLeft = false;
bool isMovingRight = false;
bool wantsToJump = false;
PlayerComponent(this.entity, this.level) {
position = entity.position;
size = entity.size;
}
@override
Future<void> onLoad() async {
// Use sprite if available, otherwise use colored rectangle
if (entity.sprite != null) {
final spriteComponent = SpriteComponent(
sprite: entity.sprite,
size: size,
);
await add(spriteComponent);
} else {
final rect = RectangleComponent(
size: size,
paint: Paint()..color = entity.color ?? Colors.blue,
);
await add(rect);
}
}
@override
void update(double dt) {
super.update(dt);
final collisions = level.intGrids['Collisions'];
if (collisions == null) return;
// Horizontal movement
velocity.x = (isMovingRight ? moveSpeed : 0) +
(isMovingLeft ? -moveSpeed : 0);
// Jump
if (wantsToJump && isOnGround) {
velocity.y = jumpForce;
isOnGround = false;
}
// Gravity
velocity.y += gravity * dt;
// Apply movement with collision detection
final newX = position.x + velocity.x * dt;
if (_canMoveTo(collisions, newX, position.y)) {
position.x = newX;
}
final newY = position.y + velocity.y * dt;
if (_canMoveTo(collisions, position.x, newY)) {
position.y = newY;
isOnGround = false;
} else {
if (velocity.y > 0) isOnGround = true;
velocity.y = 0;
}
}
bool _canMoveTo(LdtkIntGrid grid, double x, double y) {
return !grid.isSolidAtPixel(x, y) &&
!grid.isSolidAtPixel(x + size.x, y) &&
!grid.isSolidAtPixel(x, y + size.y) &&
!grid.isSolidAtPixel(x + size.x, y + size.y);
}
void onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keys) {
isMovingLeft = keys.contains(LogicalKeyboardKey.arrowLeft);
isMovingRight = keys.contains(LogicalKeyboardKey.arrowRight);
wantsToJump = keys.contains(LogicalKeyboardKey.space);
}
}
API Reference
LdtkWorld
Manages LDtk project configuration and level loading.
// Load a world
final world = await LdtkWorld.load('assets/world.ldtk');
// Access world properties
bool isSimplified = world.isSimplified; // Super Simple Export?
bool hasExternalLevels = world.hasExternalLevels; // External .ldtkl files?
String assetBasePath = world.assetBasePath; // Base path for assets
List<LdtkJsonLevel> levels = world.levels; // All available levels
// Get paths for a level
String? ldtklPath = world.getLdtklPath('Level_0');
String? bgPath = world.getBackgroundPath('Level_0');
String? levelPath = world.getSimplifiedLevelPath('Level_0');
LdtkLevelComponent
Main component for loading and displaying LDtk levels.
// Create component with world
final level = LdtkLevelComponent(world);
// Load a level by identifier
await level.loadLevel(
'Level_0',
intGridLayers: ['Collisions', 'Water'], // Optional: load collision layers
useComposite: false, // Optional: use individual layers (default)
loadBackground: true, // Optional: load background image (default)
);
// Access level data
LdtkLevel? data = level.levelData;
// Change level dynamically
await level.loadLevel('Level_1', intGridLayers: ['Collisions']);
// Override to customize entity creation
@override
Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
// Your custom entity creation logic
}
Parameters:
levelIdentifier- Name of the level in LDtk (e.g., 'Level_0', 'Dungeon_1')intGridLayers- List of IntGrid layer names to load for collisionsuseComposite- Use composite image or individual layers (default: false for transparency)loadBackground- Automatically load background image if defined (default: true)
Background images and entity sprites:
- Background images are loaded automatically from
.ldtklfiles - Entity sprites are loaded automatically if you assign a tile to an entity in LDtk
- All paths are managed by
LdtkWorld, no manual configuration needed
Supported background positioning modes:
Cover- Background covers the entire levelContain- Background scaled to fit while maintaining aspect ratioUnscaled- Background uses original size
LdtkLevel
Contains all level data.
String name; // Level identifier
int width, height; // Level dimensions in pixels
Color? bgColor; // Background color
List<LdtkEntity> entities; // All entities
Map<String, LdtkIntGrid> intGrids; // IntGrid layers by name
Map<String, dynamic> customData; // Custom fields
LdtkEntity
Represents an entity from LDtk.
String identifier; // Entity type (e.g., "Player")
Vector2 position; // Top-left position in pixels
Vector2 size; // Size in pixels
Map<String, dynamic> fields; // Custom fields
Color? color; // Color from LDtk
Sprite? sprite; // Sprite from entity tile (if assigned in LDtk)
LdtkIntGrid
Grid-based collision/logic layer.
int cellSize; // Cell size in pixels
int width, height; // Grid dimensions in cells
bool isSolid(int x, int y); // Check cell by grid coords
bool isSolidAtPixel(double x, double y); // Check by pixel coords
int getValue(int x, int y); // Get cell value (0 = empty)
Tips & Best Practices
1. Use separate components for different entity types
class PlayerComponent extends LdtkEntityComponent { ... }
class EnemyComponent extends LdtkEntityComponent { ... }
class ItemComponent extends LdtkEntityComponent { ... }
2. Store level reference for collision access
class GameEntity extends PositionComponent {
final LdtkLevel level;
GameEntity(LdtkEntity entity, this.level) {
position = entity.position;
size = entity.size;
}
}
3. Use custom fields for entity configuration
In LDtk, add custom fields to entities:
speed: Intfor movement speedhealth: Intfor HPloot: Stringfor item type
Access them in your components:
final speed = entity.fields['speed'] as int? ?? 100;
final health = entity.fields['health'] as int? ?? 3;
4. Handle different collision types
final collisions = level.intGrids['Collisions'];
final water = level.intGrids['Water'];
final spikes = level.intGrids['Hazards'];
if (collisions?.isSolidAtPixel(x, y) ?? false) {
// Hit solid wall
}
if (water?.isSolidAtPixel(x, y) ?? false) {
// In water, apply different physics
}
Roadmap
Note: I created this project for a game I'm currently developing. The roadmap may evolve based on my needs. The Super Simple Export mode is the most tested and stable format.
✅ Completed
xSuper Simple Export supportxWorld Management -LdtkWorldclass for simplified project and level managementxEntity Tiles/Sprites - Automatic sprite loading from entity tiles defined in LDtkxLevel Switching - Change levels dynamically without recreating componentsxCustom fields extractionxLRU cache system with memory limitsxImproved error handling with detailed messagesxIndividual Layer Rendering - Load and render tile layers separately (viauseComposite: falseby default)xBackground Images - Basic positioning modes (Cover, Contain, Unscaled) supported. Advanced options (custom scale, crop rectangles) not yet implemented.xJSON Export support (experimental) - See JSON_FORMAT.md
Planned features
AutoLayers Support - Render auto-generated tile layersLevel Transitions - Fade, slide, and custom transition effects between levelsParallax Backgrounds - Support for parallax effects with background imagesAdvanced Background Options - Custom scale and crop rectangle supportTile Animations - Animated tileset support with metadata parsing
Other ideas
Entity Registry/Factory - Automatic entity-to-component mapping systemCollision Generation from IntGrid - Automatic hitbox generation (polygons/rectangles)Hot Reload Support - Watch LDtk files and reload in developmentDebug Renderer - Visualize grids, entity bounds, collisions, and IntGrid valuesPlatformer Behavior Mixin - Reusable gravity and collision behaviors
🔧 Technical improvements ideas
Typed Field Values - Strong typing for Point, Color, Enum, EntityRef, Array fieldsEnum Support - Parse and use LDtk enum definitionsRender Optimization - Tile batching, atlases, and off-screen cullingLevel Streaming - Progressive loading for large levelsPNG-based IntGrid parsing - Alternative to CSV format
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Credits
Libraries
- flame_ldtk
- A Flutter package for integrating LDtk levels into Flame Engine games.