flame_ldtk

A Flutter package for integrating LDtk levels into Flame Engine games.

pub package codecov

Features

  • ๐ŸŒ World Management - Simplified API with LdtkWorld for 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

  1. Create your level in LDtk
  2. Go to Project Settings โ†’ Super Simple Export
  3. Enable Super Simple Export
  4. Set your export path (e.g., assets/world/simplified/)
  5. 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 collisions
  • useComposite - 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 .ldtkl files
  • 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 level
  • Contain - Background scaled to fit while maintaining aspect ratio
  • Unscaled - 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: Int for movement speed
  • health: Int for HP
  • loot: String for 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

  • x Super Simple Export support
  • x World Management - LdtkWorld class for simplified project and level management
  • x Entity Tiles/Sprites - Automatic sprite loading from entity tiles defined in LDtk
  • x Level Switching - Change levels dynamically without recreating components
  • x Custom fields extraction
  • x LRU cache system with memory limits
  • x Improved error handling with detailed messages
  • x Individual Layer Rendering - Load and render tile layers separately (via useComposite: false by default)
  • x Background Images - Basic positioning modes (Cover, Contain, Unscaled) supported. Advanced options (custom scale, crop rectangles) not yet implemented.
  • x JSON Export support (experimental) - See JSON_FORMAT.md

Planned features

  • AutoLayers Support - Render auto-generated tile layers
  • Level Transitions - Fade, slide, and custom transition effects between levels
  • Parallax Backgrounds - Support for parallax effects with background images
  • Advanced Background Options - Custom scale and crop rectangle support
  • Tile Animations - Animated tileset support with metadata parsing

Other ideas

  • Entity Registry/Factory - Automatic entity-to-component mapping system
  • Collision Generation from IntGrid - Automatic hitbox generation (polygons/rectangles)
  • Hot Reload Support - Watch LDtk files and reload in development
  • Debug Renderer - Visualize grids, entity bounds, collisions, and IntGrid values
  • Platformer Behavior Mixin - Reusable gravity and collision behaviors

๐Ÿ”ง Technical improvements ideas

  • Typed Field Values - Strong typing for Point, Color, Enum, EntityRef, Array fields
  • Enum Support - Parse and use LDtk enum definitions
  • Render Optimization - Tile batching, atlases, and off-screen culling
  • Level Streaming - Progressive loading for large levels
  • PNG-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

  • LDtk - Level Designer Toolkit by Sรฉbastien Benard
  • Flame - Flutter game engine
  • Kenney - Assets on example/ project

Libraries

flame_ldtk
A Flutter package for integrating LDtk levels into Flame Engine games.