flame_ldtk
A Flutter package for integrating LDtk levels into Flame Engine games.
Features
- 🎮 Super Simple Export Support - Optimized loading of LDtk levels using Super Simple Export format
- 🗺️ Level Rendering - Individual layer rendering with transparency support
- 🖼️ Background Images - Load background images from .ldtkl files (basic positioning modes)
- 🎯 Entity Parsing - Extract entities with positions, sizes, custom fields, and colors
- 🧱 IntGrid Support - CSV-based IntGrid for collisions and game logic
- 🎨 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/
- assets/world/Level_0.ldtkl # Optional: for background images
- assets/background.png # Optional: your background image
2. Load a level in your game
Basic usage:
import 'package:flame/game.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final level = LdtkLevelComponent();
await level.loadLevel('assets/world/simplified/Level_0');
await add(level);
}
}
With collisions and background image:
import 'package:flame/game.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final level = LdtkLevelComponent();
await level.loadLevel(
'assets/world/simplified/Level_0',
intGridLayers: ['Collisions'], // Load collision layer
ldtklPath: 'assets/world/Level_0.ldtkl', // For background image
assetBasePath: 'assets', // Where background images are located
// useComposite: false, // Default: loads individual layers
);
await add(level);
}
}
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 {
final level = MyLevelComponent();
await level.loadLevel(
'assets/world/simplified/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;
@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 {
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
LdtkLevelComponent
Main component for loading and displaying LDtk levels.
// Load a level
await levelComponent.loadLevel(
'assets/world/simplified/Level_0',
intGridLayers: ['Collisions', 'Water'], // Optional
);
// Access level data
LdtkLevel? data = levelComponent.levelData;
// Override to customize entity creation
@override
Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
// Your custom entity creation logic
}
Background Images
Super Simple Export doesn't include background image metadata in its exported files. To use background images, you need to read the background configuration from the original .ldtkl file:
await levelComponent.loadLevel(
'assets/world/simplified/Level_0',
intGridLayers: ['Collisions'],
ldtklPath: 'assets/world/Level_0.ldtkl', // Path to .ldtkl file for background metadata
assetBasePath: 'assets', // Base path for resolving background images
useComposite: false, // Use individual layers for transparency
);
Parameters:
ldtklPath- Path to the.ldtklfile containing background image configurationassetBasePath- Base path for resolving background image paths (useful when LDtk'sbgRelPathis relative to a specific folder)useComposite- Set tofalseto load individual layer images instead of the composite (allows transparency for backgrounds to show through)
Supported background positioning modes:
Cover- Background covers the entire level (default)Contain- Background is scaled to fit within level bounds while maintaining aspect ratioUnscaled- Background uses its original size
Note: Advanced LDtk background options (custom scale, crop rectangles) are not currently supported.
Example in pubspec.yaml:
flutter:
assets:
- assets/world/simplified/Level_0/ # Super Simple Export files
- assets/world/Level_0.ldtkl # For background metadata
- assets/background.png # Your background image
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
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 supportxCustom 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 (partial) - 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 layersMulti-Level World System - World component with level switching and transitionsParallax 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 valuesLevel Transitions - Fade, slide, and custom transition effectsPlatformer 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.