Skip to content

thomascarvalho/flame_ldtk

Repository files navigation

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

flutter pub add flame_ldtk

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 {
    await super.onLoad();

    // 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);

    // Optional: Center camera on the level
    camera.viewfinder.position = Vector2(
      level.levelData!.width / 2,
      level.levelData!.height / 2,
    );
  }
}

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 {
    await super.onLoad();

    // Load the world - it handles all paths automatically
    final world = await LdtkWorld.load('assets/world.ldtk');

    // Create and load a level with collision layer
    // Background and entity sprites are loaded automatically
    final level = LdtkLevelComponent(world);
    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 {
    await super.onLoad();

    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 {
  Player? player;

  MyLevelComponent(super.world);

  @override
  Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
    for (final entity in entities) {
      switch (entity.identifier) {
        case 'Player':
          player = Player(entity, levelData!);
          await add(player!);
          break;

        case 'Enemy':
          final enemy = Enemy(entity, levelData!);
          await add(enemy);
          break;

        case 'Coin':
          final coin = Coin(entity);
          await add(coin);
          break;
      }
    }
  }
}

Create entity components

class Player extends PositionComponent {
  final LdtkEntity entity;
  final LdtkLevel level;

  Player(this.entity, this.level) {
    position = entity.position;  // Position from LDtk
    size = entity.size;           // Size from LDtk
  }

  @override
  Future<void> onLoad() async {
    // entity.sprite - Sprite from LDtk tile (if assigned)
    // entity.color - Color from LDtk
    // Add your rendering logic here
    ...
  }
}

Access custom fields

class Chest extends PositionComponent {
  Chest(LdtkEntity 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;

    // Use the custom fields in your game logic
    ...
  }
}

Working with IntGrid (Collisions)

Load IntGrid layers

// When loading a level, specify which IntGrid layers to load
await level.loadLevel(
  'Level_0',
  intGridLayers: ['Collisions', 'Water', 'Hazards'],  // Load IntGrid layers
);

Implement collision detection

class Player extends PositionComponent {
  final LdtkLevel level;

  @override
  void update(double dt) {
    // Access IntGrid layers loaded from LDtk
    final collisions = level.intGrids['Collisions'];
    if (collisions == null) return;

    // Use IntGrid methods to check collisions
    final canMove = !collisions.isSolidAtPixel(newX, newY);

    // Your game physics logic here
    ...
  }
}

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_ldtk/flame_ldtk.dart';

// 1. Load the world and level
class MyGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    await super.onLoad();

    // Load the LDtk world
    final world = await LdtkWorld.load('assets/world.ldtk');

    // Create custom level component
    final level = MyLevelComponent(world);
    await level.loadLevel('Level_0', intGridLayers: ['Collisions']);

    await add(level);

    // Center camera on the level
    camera.viewfinder.position = Vector2(
      level.levelData!.width / 2,
      level.levelData!.height / 2,
    );
  }
}

// 2. Override onEntitiesLoaded to handle entities from LDtk
class MyLevelComponent extends LdtkLevelComponent {
  Player? player;

  MyLevelComponent(super.world);

  @override
  Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
    for (final entity in entities) {
      switch (entity.identifier) {
        case 'Player':
          player = Player(entity, levelData!);
          await add(player!);
          break;
        case 'Enemy':
          await add(Enemy(entity, levelData!));
          break;
      }
    }
  }
}

// 3. Use entity data and IntGrid for collision detection
class Player extends PositionComponent {
  final LdtkEntity entity;
  final LdtkLevel level;

  Player(this.entity, this.level) {
    // Get position and size from LDtk entity
    position = entity.position;
    size = entity.size;
  }

  @override
  Future<void> onLoad() async {
    // Use sprite from LDtk if available
    if (entity.sprite != null) {
      await add(SpriteComponent(sprite: entity.sprite, size: size));
    }
    // Your rendering logic here
    ...
  }

  @override
  void update(double dt) {
    // Access IntGrid for collision detection
    final collisions = level.intGrids['Collisions'];
    if (collisions == null) return;

    // Check collisions using IntGrid methods
    if (!collisions.isSolidAtPixel(newX, newY)) {
      position.x = newX;
    }

    // Your game physics here
    ...
  }
}

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 Player extends PositionComponent { ... }
class Enemy extends PositionComponent { ... }
class Item extends PositionComponent { ... }

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

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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors