LDTK Integration

Introduction to LDTK

Level Designer Toolkit (LDTK) is a modern, open-source level editor that makes creating game levels fast and intuitive. LDTK provides a powerful visual interface for designing levels, with support for:

  • Tile layers
  • Entity placement
  • Int grid layers (for collision and other metadata)
  • Auto-tiling rules
  • Multi-layered level design

Bappa’s LDTK integration allows you to harness these capabilities while maintaining the component-based architecture that makes Bappa powerful.

Getting Started

Prerequisites

  • The standard Bappa game engine setup
  • LDTK editor installed on your development machine

Project Structure

When using LDTK with Bappa, your project will typically include:

your-game/
├── ldtk/
│   ├── data.ldtk         # Your LDTK project file
├── scenes/
│   ├── scene.go          # Scene definitions
│   ├── scene_one.go      # Individual scene plans
│   └── scene_two.go
└── assets/
    └── images/
        └── tilesets/     # Tilesets referenced by LDTK

Ideally all asset files that your LDTK project references will be in the local asset folder.

Creating the LDTK Project

Basic Setup

  1. Design your levels using the LDTK editor
  2. Save your LDTK project file as ldtk/data.ldtk
  3. Reference your tilesets from the assets/images/tilesets directory

Embedding the LDTK Data

In your ldtk/ldtk.go file:

package ldtk

import (
 "embed"
 "log"

 "github.com/TheBitDrifter/bappa/blueprint/ldtk"
)

//go:embed data.ldtk
var data embed.FS

var DATA = func() *ldtk.LDtkProject {
 project, err := ldtk.Parse(data, "./ldtk/data.ldtk")
 if err != nil {
  log.Fatal(err)
 }
 return project
}()

This loads your LDTK project file and makes it available to your game code.

Loading LDTK Levels in Bappa

Creating Scene Plans

Create a scene plan that loads your LDTK level:

package scenes

import (
 "github.com/TheBitDrifter/yourgame/package_where_ldtk_data_is_stored"
 "github.com/TheBitDrifter/warehouse"
)

const SCENE_ONE_NAME = "Scene1"

var SceneOne = Scene{
 Name:   SCENE_ONE_NAME,
 Plan:   sceneOnePlan,
 Width:  package_where_ldtk_data_is_stored.DATA.WidthFor(SCENE_ONE_NAME),
 Height: package_where_ldtk_data_is_stored.DATA.HeightFor(SCENE_ONE_NAME),
}

func sceneOnePlan(height, width int, sto warehouse.Storage) error {
 // Load the image tiles
 err := package_where_ldtk_data_is_stored.DATA.LoadTiles(SCENE_ONE_NAME, sto)
 if err != nil {
  return err
 }

 // Load the terrain collision
 blockArchetype, _ := sto.NewOrExistingArchetype(BlockTerrainComposition...)
 platArchetype, _ := sto.NewOrExistingArchetype(PlatformComposition...)
 transferArchetype, _ := sto.NewOrExistingArchetype(CollisionPlayerTransferComposition...)

 err = package_where_ldtk_data_is_stored.DATA.LoadIntGrid(SCENE_ONE_NAME, sto, blockArchetype, platArchetype, transferArchetype)
 if err != nil {
  return err
 }

 // Load custom LDTK entities
 err = package_where_ldtk_data_is_stored.DATA.LoadEntities(SCENE_ONE_NAME, sto, entityRegistry)
 if err != nil {
  return err
 }

 // Add other scene elements (music, background, etc.)
 err = NewJazzMusic(sto)
 if err != nil {
  return err
 }
 return NewCityBackground(sto)
}

LDTK Layer Types

Bappa’s LDTK integration supports all major LDTK layer types:

Tile Layers

Tile layers are loaded as visual elements. Each tile layer becomes a single entity with a sprite bundle containing all tiles:

// In your scene plan:
err := package_where_ldtk_data_is_stored.DATA.LoadTiles(SCENE_NAME, sto)
if err != nil {
 return err
}

Int Grid Layers

Int grid layers are typically used for collision or metadata. Each value in the int grid corresponds to a different type of collider or behavior:

// In your scene plan:
// First prepare archetypes for each int grid value
blockArchetype, _ := sto.NewOrExistingArchetype(BlockTerrainComposition...)
platformArchetype, _ := sto.NewOrExistingArchetype(PlatformComposition...)
// ... other archetypes

// Then load the int grid, passing archetypes in order of int grid values (1, 2, 3, etc.)
err = package_where_ldtk_data_is_stored.DATA.LoadIntGrid(SCENE_NAME, sto, blockArchetype, platformArchetype)

Entity Layers

Entity layers are loaded using the entity handlers you registered with the entityRegistry:

// In your scene plan:
err = package_where_ldtk_data_is_stored.DATA.LoadEntities(SCENE_NAME, sto, entityRegistry)

Working Custom LDTK Entities

First, you need to register handlers for each LDTK entity type. In your scenes/scene.go:

package scenes

import (
 "github.com/TheBitDrifter/blueprint"
 "github.com/TheBitDrifter/warehouse"

 "github.com/TheBitDrifter/blueprint/ldtk"
)

var entityRegistry = blueprintldtk.NewLDtkEntityRegistry()

// Registering custom LDTK entities
func init() {
 // Player start position handler
 entityRegistry.Register("PlayerStart", func(entity *ldtk.LDtkEntityInstance, sto warehouse.Storage) error {
  // Create player at the position defined in LDtk
  return NewPlayer(float64(entity.Position[0]), float64(entity.Position[1]), sto)
 })

 // Scene transition trigger handler
 entityRegistry.Register("SceneTransfer", func(entity *ldtk.LDtkEntityInstance, sto warehouse.Storage) error {
  // Extract properties from LDtk entity
  targetScene := entity.StringFieldOr("targetScene", "Scene2") // Default if not specified
  targetX := entity.FloatFieldOr("targetX", 20.0)
  targetY := entity.FloatFieldOr("targetY", 400.0)
  width := entity.FloatFieldOr("width", 100)
  height := entity.FloatFieldOr("height", 100)

  // Create the transfer trigger
  return NewCollisionPlayerTransfer(
   sto,
   float64(entity.Position[0]),
   float64(entity.Position[1]),
   width,
   height,
   targetX,
   targetY,
   targetScene,
  )
 })

 // Register other entity types...
}

LDTK allows you to define custom properties for your entities. Access these properties in your entity handlers:

entityRegistry.Register("Enemy", func(entity *ldtk.LDtkEntityInstance, sto warehouse.Storage) error {
 // Get entity properties
 enemyType := entity.StringFieldOr("type", "basic")
 health := entity.IntFieldOr("health", 100)
 patrolSpeed := entity.FloatFieldOr("patrolSpeed", 2.0)

 // Create the enemy with these properties
 return NewEnemy(
  float64(entity.Position[0]),
  float64(entity.Position[1]),
  enemyType,
  health,
  patrolSpeed,
  sto,
 )
})

Conclusion

LDTK integration brings powerful level design capabilities to your Bappa games. By separating level design from code, you can iterate faster and create more complex game worlds. The integration preserves Bappa’s component-based architecture while adding the visual editing capabilities of LDTK.

For advanced use cases, you can extend the integration with custom entity handlers, specialized collision systems, or even runtime level generation. The foundation provided here gives you a solid starting point for creating engaging, visually rich game levels in Bappa.