Skip to content

LDtk Overview

Shmellyorc edited this page Sep 3, 2026 · 1 revision

The Problem

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.

What LDtk Is

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.

What Void Does

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.

Performance First

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.

How The Hash Lookup Works

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.

GC Friendly

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.

Strongly Typed

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
}

Caching

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

Tileset Integration

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
}

Entity Lookup

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
}

Data Structure

An LDtk project contains levels. Each level contains layers. Layers contain instances.

LDtkMap
    └── LDtkLevel
            └── MapLayer
                    ├── LDtkEntityInstance
                    ├── LDtkTileInstance
                    └── LDtkIntGridInstance

Loading a Map

var map = AssetManager.Instance.Load<LDtkMap>("levels/world.ldtk");

The AssetManager handles caching. Once loaded, the map stays in memory until evicted.

Accessing Levels

// By name
var level = map.GetLevelByName("Level_01");

// By ID
var level = map.GetLevelById("abc-123-def-456");

Accessing Layers

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;
    }
}

Accessing Entities

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

Accessing Tiles

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;
}

Accessing Int Grid

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>();
}

What You Get

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

File Location

LDtk files are stored in your Content folder. The AssetManager finds them through the mount system.

Content/
└── levels/
    └── world.ldtk

Quick Example

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

Back to Home

Clone this wiki locally