World

github.com/mechanical-lich/mlge/world

A generic, tile-based world system using Go generics. Two type parameters let each game attach custom data to tile definitions and individual tiles without forking the package.

  • TD — Custom data on tile definitions (e.g., Door bool, Wall bool, MixGroup int)
  • T — Custom data on individual tiles (e.g., Explored bool, Temperature float64)

The package handles tile storage (3D grid with Depth=1 for 2D games), tile definition registries, A* pathfinding integration, and save/load — but intentionally excludes drawing and entity management, which are game-specific.

Quick Start

import "github.com/mechanical-lich/mlge/world"

// 1. Define your game-specific data types
type MyTileDef struct {
    Door bool `json:"door"`
    Wall bool `json:"wall"`
}

type MyTileExtra struct {
    Explored bool `json:"explored"`
}

// 2. Create a tile registry and load definitions
registry := world.NewTileRegistry[MyTileDef]()
err := registry.LoadFromFile("data/tile_definitions.json")

// 3. Create a level (10x10, single Z layer for 2D)
level := world.NewLevel[MyTileDef, MyTileExtra](10, 10, 1, registry)

// 4. Use it — type params are inferred on all method calls
_ = level.SetTileAt(5, 5, 0, "stone_wall", 0)
tile := level.GetTileAt(5, 5, 0)
tile.CustomData.Explored = true

def := level.TileDefinitionAt(5, 5, 0)
if def.CustomData.Door {
    // game-specific logic
}

Tile Definitions

TileVariant

type TileVariant struct {
    Variant int `json:"variant"`
    SpriteX int `json:"spriteX"`
    SpriteY int `json:"spriteY"`
}

A single visual variant pointing to a sprite location.

TileDefinition[TD]

type TileDefinition[TD any] struct {
    Name       string        `json:"name"`
    Solid      bool          `json:"solid"`
    Water      bool          `json:"water"`
    Variants   []TileVariant `json:"variants"`
    CustomData TD            `json:"customData"`
}

Defines a type of tile. Solid and Water are common enough to be built-in; everything else goes in CustomData.

TileRegistry[TD]

Holds the loaded definitions and provides name/index lookups.

type TileRegistry[TD any] struct {
    Definitions []TileDefinition[TD]
    NameToIndex map[string]int
    IndexToName []string
}

Methods:

Method Signature Description
NewTileRegistry NewTileRegistry[TD any]() *TileRegistry[TD] Creates an empty registry
LoadFromFile (path string) error Reads a JSON array of tile definitions
LoadFromDefinitions (defs []TileDefinition[TD]) Populates from a pre-built slice
GetByName (name string) (*TileDefinition[TD], int) Lookup by name; returns nil, -1 if not found
GetByIndex (index int) *TileDefinition[TD] Lookup by index; returns nil if out of range

JSON Format

[
  {
    "name": "grass",
    "solid": false,
    "water": false,
    "variants": [
      { "variant": 0, "spriteX": 0, "spriteY": 0 },
      { "variant": 1, "spriteX": 1, "spriteY": 0 }
    ],
    "customData": { "door": false, "wall": false }
  },
  {
    "name": "stone_wall",
    "solid": true,
    "water": false,
    "variants": [
      { "variant": 0, "spriteX": 3, "spriteY": 0 }
    ],
    "customData": { "door": false, "wall": true }
  }
]

Tile

type Tile[T any] struct {
    Type       int `json:"type"`       // index into TileRegistry.Definitions; -1 = empty
    Variant    int `json:"variant"`    // variant index
    X, Y, Z   int                     // grid coordinates
    CustomData T   `json:"customData"` // game-specific per-tile data
}

Individual tiles are stored by value in a flat array — no pointer indirection. Access them via Level.GetTileAt() which returns a pointer into the array.

IsEmpty

func (t *Tile[T]) IsEmpty() bool

Returns true when Type < 0 (no tile type has been assigned). Newly created levels start all tiles empty. Use this instead of comparing Type to a sentinel constant:

if tile.IsEmpty() {
    // skip unset tiles
}

Level

Level[TD, T]

type Level[TD any, T any] struct {
    Width, Height, Depth int
    Registry             *TileRegistry[TD]
    CustomData           any  // arbitrary level-wide state (time of day, weather, etc.)
}

Tiles are stored internally in a flat 1D array of size Width × Height × Depth. For 2D games, use Depth=1. Coordinates are (x, y, z) where z is the vertical layer.

Construction

level := world.NewLevel[MyTileDef, MyTileExtra](width, height, depth, registry)

All tiles are initialized with correct X, Y, Z coordinates and internal back-pointers for neighbor lookups. Every tile starts empty — tile.IsEmpty() returns true until a tile type is assigned via SetTileAt.

Tile Access

Method Signature Description
GetTileAt (x, y, z int) *Tile[T] Returns tile pointer, or nil if out of bounds
SetTileAt (x, y, z int, tileType string, variant int) error Sets tile type by name; errors on unknown type or OOB
ClearTileAt (x, y, z int) error Resets tile to empty (Type=-1, Variant=0); errors if OOB
GetTileIndex (index int) *Tile[T] Direct flat-array access by index
TileDefinitionAt (x, y, z int) *TileDefinition[TD] Returns the definition for the tile at the given position
InBounds (x, y, z int) bool Bounds check
TileCount () int Total number of tiles

Iteration

level.ForEachTile(func(tile *world.Tile[MyTileExtra]) {
    tile.CustomData.Explored = false
})

Viewport

// Get a 2D slice of tiles at Z=0, starting at (cameraX, cameraY)
view := level.GetView(cameraX, cameraY, 0, viewWidth, viewHeight, false)

// Centered on the player
view := level.GetView(playerX, playerY, 0, viewWidth, viewHeight, true)

Returns [][]*Tile[T] — out-of-bounds positions are nil.

Pathfinding

The world package integrates with mlge/path through a PathableTile wrapper. Games provide callback functions for passability, cost, and heuristics.

PathConfig[T]

type PathConfig[T any] struct {
    IsPassable         func(tile *Tile[T]) bool
    Cost               func(from, to *Tile[T]) float64
    EstimatedCost      func(from, to *Tile[T]) float64
    Include3DNeighbors bool
}
Field Description
IsPassable Returns true if the tile can be traversed
Cost Movement cost between adjacent tiles (default: 1.0)
EstimatedCost Heuristic estimate to target (default: Manhattan distance)
Include3DNeighbors When true, also considers Z±1 as neighbors

Usage

config := level.NewPathConfig(
    func(tile *world.Tile[MyTileExtra]) bool {
        if tile.IsEmpty() {
            return false // empty tiles are not passable
        }
        def := level.Registry.GetByIndex(tile.Type)
        return def != nil && !def.Solid
    },
    func(from, to *world.Tile[MyTileExtra]) float64 {
        if level.Registry.GetByIndex(to.Type).Water {
            return 3.0 // water tiles cost more
        }
        return 1.0
    },
    nil, // default Manhattan distance heuristic
)

from := level.GetPathableTile(startX, startY, 0, config)
to := level.GetPathableTile(goalX, goalY, 0, config)

result, distance, found := path.Path(from, to)
if found {
    for _, node := range result {
        pt := node.(*world.PathableTile[MyTileExtra])
        fmt.Printf("Step: %d, %d\n", pt.Tile.X, pt.Tile.Y)
    }
}

3D Pathfinding

For games with multiple Z levels (dungeons, multi-story buildings):

config.Include3DNeighbors = true

This adds Z±1 neighbors to the pathfinding graph alongside the 4 cardinal directions.

Utilities

SetTileTypeAndVariant

world.SetTileTypeAndVariant(tile, registry, "grass", 0)

Sets the Type and Variant on a tile by looking up the name in the registry.

RandomVariant

variant := world.RandomVariant(registry, "grass")

Returns a random variant index for the named tile type.

CreateTileCluster

world.CreateTileCluster(level, x, y, z, size, "flowers", 0)

Places a random-walk cluster of tiles around the given position. Useful for terrain generation (flower patches, ore veins, etc.).

Save / Load

The package provides JSON-based save/load that preserves all tile data including custom data. Games can attach additional save state (entities, settlements, etc.) via a CustomData field.

SaveData[T]

type SaveData[T any] struct {
    Width      int       `json:"width"`
    Height     int       `json:"height"`
    Depth      int       `json:"depth"`
    Tiles      []Tile[T] `json:"tiles"`
    CustomData any       `json:"customData,omitempty"`
}

Saving

// Save with game-specific extra data
gameState := map[string]any{"hour": 14, "day": 7}
err := world.SaveToFile(level, "save.json", gameState)

// Or get a snapshot without writing to disk
snapshot := world.SaveLevel(level)

SaveLevel returns a copy — modifying the snapshot won’t affect the live level.

Loading

level, rawCustomData, err := world.LoadFromFile[MyTileDef, MyTileExtra]("save.json", registry)

// rawCustomData is json.RawMessage — unmarshal into your game's type
var gameState MyGameState
json.Unmarshal(rawCustomData, &gameState)

The loaded level has all tile coordinates and internal back-pointers restored, so pathfinding works immediately.

Design Notes

  • No drawing — Games implement rendering using GetTileAt() / GetView() with their own camera and sprite systems.
  • No entity storage — Games manage their own entity lists and spatial indexes; the world package is tiles-only.
  • Flat 1D array — Tiles are stored densely in Width × Height × Depth order. Every position has a tile (no sparse storage).
  • Generic registry instead of package vars — Go doesn’t support generic package-level variables, so TileRegistry[TD] is an explicit struct. This also allows multiple registries if needed.
  • PathableTile wrapper — Rather than requiring Tile to implement path.Pather directly (which would bake in game-specific logic), a wrapper with function hooks keeps pathfinding configurable.

Back to top

Copyright © 2026. Distributed under the MIT License.

This site uses Just the Docs, a documentation theme for Jekyll.