-
Notifications
You must be signed in to change notification settings - Fork 1
LDtk Overview
Level design is one of the most time-consuming parts of game development. You need a tool that lets you place tiles, entities, and objects quickly. You need to iterate fast. You need to see your changes immediately.
Most engines have their own level editors. They are often clunky, slow, or require you to write custom tools. Some engines don't have an editor at all, leaving you to hand-write level data in JSON or XML.
And when you finally have your levels, you need to load them into your game. That means parsing files, building collision maps, creating entities, and managing tiles. It's a lot of work for something that should be simple.
LDtk is a free, open-source 2D level editor. It was created by Sébastien Bénard, the developer behind Dead Cells. It's designed specifically for 2D games, with a focus on speed and simplicity.
LDtk supports everything you need for 2D level design:
- Tile layers with tilesets
- Auto-layers for automatic tiling
- Entity layers for placing game objects
- Int grid layers for collision and terrain data
- Custom fields for adding data to levels, layers, and entities
It's fast. It's intuitive. And it exports to JSON, which makes it easy to load in any game engine.
Void Engine includes a complete LDtk integration. You don't need to write your own parser. You don't need to manually build collision maps. You don't need to convert JSON to game objects.
Void loads LDtk files directly through the AssetManager. It parses the JSON once and builds caches for fast lookups. You get strongly-typed access to every level, layer, entity, tile, and setting.
Most LDtk importers parse the JSON and give you raw data. You are left to build your own lookup tables, cache entities by ID, and manually manage references. This adds complexity to your game code and slows down development.
Void does this work for you.
The LDtk integration is built with performance in mind. Every lookup is O(1), not O(n). Levels, layers, entities, and tilesets are stored in dictionaries keyed by hash. This means no linear searches. No scanning lists. No string comparisons every frame.
| Lookup Type | Traditional Approach | Void Approach |
|---|---|---|
| Level by name | O(n) scan | O(1) hash lookup |
| Level by ID | O(n) scan | O(1) hash lookup |
| Layer by ID | O(n) scan | O(1) hash lookup |
| Entity by ID | O(n) scan | O(1) hash lookup |
| Tileset by name | O(n) scan | O(1) hash lookup |
When you load an LDtk map, Void parses the JSON once and builds all the caches immediately. The data is then accessible in constant time. No matter how many levels, layers, or entities you have, lookups are instant.
The O(1) lookups are powered by HashHelper, which generates FNV-1a hashes of strings. When an LDtk map loads, every level, layer, entity, and tileset is hashed once and stored in a dictionary. From that point on, lookups are just dictionary lookups.
When you call GetLevelByName("Level_01"), the string "Level_01" is hashed once and used as a key into the dictionary. No string comparisons are performed. No linear scans. Just a single hash computation and a dictionary lookup.
HashHelper is designed to be GC friendly. Small strings are hashed on the stack with no heap allocation. Large strings use the ArrayPool to rent and return buffers, minimizing GC pressure.
| Hash Operation | Allocation |
|---|---|
| Small string (≤ 256 chars) | Stack allocation, no heap |
| Large string (> 256 chars) | ArrayPool rental, minimal GC |
| Cached hash (first access) | Lazy allocation |
| Cached hash (subsequent) | No allocation |
This means your LDtk lookups are not just fast, they are also memory efficient. No allocations means less GC pressure. Less GC pressure means smoother gameplay.
Void gives you strongly typed access to everything. No string parsing. No manual conversions.
// Entity name, position, size, pivot, and tags are all typed
string name = entity.Name;
Vect2 position = entity.Position;
Vect2 size = entity.Size;
Vect2 pivot = entity.Pivot;
List<string> tags = entity.Tags;
// Settings are strongly typed with Try methods
if (LDtkSetting.TryGetIntSetting(entity.Settings, "Health", out int health))
{
// Use health as an int
}
if (LDtkSetting.TryGetEnumSetting<EnemyType>(entity.Settings, "Type", out var type))
{
// Use type as an enum
}Void caches everything by ID and name. Once a map is loaded, all lookups are instant.
// First access: loads and caches the map
var map = AssetManager.Instance.Load<LDtkMap>("levels/world.ldtk");
// Subsequent access: instant cache hit
var level = map.GetLevelByName("Level_01");
var entity = map.GetEntityById("abc-123-def-456");Tilesets are loaded through the AssetManager. Void provides helper methods to load tileset textures directly from the LDtk map.
// Load tileset texture for a specific tileset ID
var texture = AssetManager.Instance.LoadTilesetTexture(map, tilesetId);
// Or use the try pattern
if (AssetManager.Instance.TryLoadTilesetTexture(map, tilesetId, out var texture))
{
// Use the texture
}Entities can be looked up by ID from anywhere in the map. This is useful for modding, debugging, or any system that needs to reference specific entities.
// Get an entity anywhere in the map
var entity = map.GetEntityById("entity_id");
// Or try get with fallback
if (map.TryGetEntityById("entity_id", out var entity))
{
// Use the entity
}An LDtk project contains levels. Each level contains layers. Layers contain instances.
LDtkMap
└── LDtkLevel
└── MapLayer
├── LDtkEntityInstance
├── LDtkTileInstance
└── LDtkIntGridInstance
var map = AssetManager.Instance.Load<LDtkMap>("levels/world.ldtk");The AssetManager handles caching. Once loaded, the map stays in memory until evicted.
// By name
var level = map.GetLevelByName("Level_01");
// By ID
var level = map.GetLevelById("abc-123-def-456");var layer = map.GetLayerById("layer_id");
// Or iterate through a level's layers
foreach (var layer in level.Layers)
{
// Check the layer type
switch (layer.Type)
{
case LDtkLayerType.Entities:
var entities = layer.InstanceAs<LDtkEntityInstance>();
break;
case LDtkLayerType.Tiles:
var tiles = layer.InstanceAs<LDtkTileInstance>();
break;
case LDtkLayerType.IntGrid:
var grid = layer.InstanceAs<LDtkIntGridInstance>();
break;
}
}// Get all entities in a layer
var entities = layer.InstanceAs<LDtkEntityInstance>();
foreach (var entity in entities)
{
Console.WriteLine($"Entity: {entity.Name} at {entity.Position}");
// Access entity settings
var health = LDtkSetting.GetIntSetting(entity.Settings, "Health");
}var tiles = layer.InstanceAs<LDtkTileInstance>();
foreach (var tile in tiles)
{
// Get the source rectangle in the tileset
Rect2 source = tile.Source;
// Check if the tile is flipped
if (tile.Effects.HasFlag(TextureEffects.Horizontal))
{
// Render flipped horizontally
}
// Get position
Vect2 position = tile.Position;
}var gridValues = layer.InstanceAs<LDtkIntGridInstance>();
foreach (var cell in gridValues)
{
// Check if the cell is solid
if (cell.IsSolid)
{
// Handle solid cell
}
// Get the index as an enum
var terrainType = cell.IndexAsEnum<TerrainType>();
}| Feature | Description |
|---|---|
| Full LDtk support | All layer types: Entities, Tiles, IntGrid, AutoLayer |
| Strongly typed | No string parsing. Every value is typed. |
| Fast lookups | Levels, layers, entities, and tilesets are cached by ID and name |
| Entity settings | Access custom fields with type-safe methods |
| Tile effects | Horizontal and vertical flips are handled automatically |
| Int grid values | Access grid data as integers or enums |
| Tileset loading | Tileset textures load through the AssetManager |
| AssetManager integration | LDtk maps are loaded and cached like any other asset |
LDtk files are stored in your Content folder. The AssetManager finds them through the mount system.
Content/
└── levels/
└── world.ldtk
// Load the map
var map = AssetManager.Instance.Load<LDtkMap>("levels/world.ldtk");
// Get the first level
var level = map.GetLevelByName("Level_01");
// Find all entities in the level
foreach (var layer in level.Layers)
{
if (layer.Type == LDtkLayerType.Entities)
{
var entities = layer.InstanceAs<LDtkEntityInstance>();
foreach (var entity in entities)
{
Console.WriteLine($"Found entity: {entity.Name} at {entity.Position}");
}
}
}Home · Getting Started · Rendering · Custom Renderers · GitHub · Report an Issue
Built with VOID Engine · MIT License