flame_ldtk 0.2.0
flame_ldtk: ^0.2.0 copied to clipboard
A Flutter package for integrating LDtk levels into Flame Engine games
flame_ldtk #
A Flutter package for integrating LDtk levels into Flame Engine games.
Features #
- ๐ฎ Dual Format Support - Load LDtk levels in both Super Simple Export and standard JSON format
- ๐บ๏ธ Level Rendering - Automatic composite image loading and rendering
- ๐ฏ 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 - Shared utilities and centralized caching for both parsers
Installation #
Add flame_ldtk to your pubspec.yaml:
dependencies:
flame: ^1.32.0
flame_ldtk: ^0.2.0
LDtk Setup #
This package supports two LDtk export formats:
Option 1: Super Simple Export (Recommended for mobile/web) #
Best for: Fast loading, minimal memory usage, mobile/web games
- 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 visualdata.json- Level metadata and entities (489B for simple levels)[LayerName].csv- IntGrid layers (for collisions, etc.)
Option 2: Standard JSON Export #
Best for: Access to full project definitions, fewer files per level
- Create your level in LDtk
- In Project Settings, enable "Save levels to separate files" (optional)
- Save your project to generate
.ldtkand.ldtklfiles
Your project structure will be:
world.ldtk- Main project file with definitionsworld/Level_0.ldtkl- Individual level files (if using external levels)
Basic Usage #
1. Add assets to pubspec.yaml #
flutter:
assets:
# For Super Simple Export
- assets/world/simplified/Level_0/
# For JSON format
- assets/world.ldtk
- assets/world/
2. Load a level in your game #
Using Super Simple Format (Recommended)
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);
}
}
Using JSON Format
import 'package:flame/game.dart';
import 'package:flame_ldtk/flame_ldtk.dart';
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final level = LdtkJsonLevelComponent();
await level.loadLevel('assets/world.ldtk', 'Level_0');
await add(level);
}
}
Note: Both components provide the same API! The only difference is the format they load.
Working with Entities #
Customize entity rendering #
Override onEntitiesLoaded() to handle your entities (works with both components):
// Works with both LdtkLevelComponent and LdtkJsonLevelComponent!
class MyLevelComponent extends LdtkLevelComponent { // or LdtkJsonLevelComponent
@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 (top-left corner)
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 (Super Simple Format) #
Main component for loading and displaying LDtk levels in Super Simple Export format.
// 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
}
LdtkJsonLevelComponent (JSON Format) #
Component for loading and displaying LDtk levels in standard JSON format.
// Load a level
await levelComponent.loadLevel(
'assets/world.ldtk', // Project file
'Level_0', // Level identifier
);
// Access level data (same as LdtkLevelComponent)
LdtkLevel? data = levelComponent.levelData;
// Override to customize entity creation (same API)
@override
Future<void> onEntitiesLoaded(List<LdtkEntity> entities) async {
// Your custom entity creation logic
}
Both components share the same API - just swap them based on your export format!
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
}
Choosing the Right Parser #
Use Super Simple Parser when: #
- ๐ You need fast loading times
- ๐ฑ Building for mobile or web
- ๐พ Memory usage is a concern
- ๐ฎ You have many levels to load dynamically
Use JSON Parser when: #
- ๐ You need access to entity/tileset definitions
- ๐ฆ You prefer fewer files (one .ldtkl per level)
- ๐ง You want to access metadata from the project file
- ๐ฅ๏ธ Building for desktop with ample resources
Performance Comparison (Level_0):
Super Simple: ~500B JSON + optional assets = Fast, minimal RAM
JSON: ~16KB project + level data = More features, higher RAM
Roadmap #
- โ Super Simple Export support
- โ JSON Export support
- โ Custom fields extraction for both formats
- โ Shared utilities and optimized performance
- โ Multi-level support with transitions
- โ PNG-based IntGrid parsing
- โ Tile layer support (individual tiles)
- โ Level background rendering
- โ Hot reload support for LDtk changes
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
MIT License - see LICENSE file for details.