Skip to content

Backends & wiring

NPCs need to find routes through your world, around walls, through corridors, and across tile maps. golem-engine provides a nav backend system: you pick a backend, build a walkability grid from your map data at startup, and the server’s FindPath delegates to it automatically.

The nav system is pluggable: any type that implements golem.NavBackend works. Two backends ship out of the box:

  • github.com/demiurgos-hub/golem-engine/golem/nav/pathing: grid A* built on quasilyte/pathing. Zero-allocation, the fastest option. Passable/impassable only (no variable terrain costs). Requires external synchronisation if SetNavWalkable and FindPath are called from different goroutines.
  • github.com/demiurgos-hub/golem-engine/golem/nav/kelindar: grid A* built on kelindar/tile. Thread-safe concurrent reads and writes. Supports variable per-cell traversal costs (CostOf). Adds Around for BFS radius scans, WriteValue/ReadValue for runtime terrain changes, and a Grid() accessor for observers, save/load, and bounded tile iteration.

Both have the same constructor signatures (New, NewFromTiledLayer, NewFromLDtkIntGrid) so switching is a one-line import change. If you need a completely different algorithm (flow fields, navmesh, a waypoint graph), implement golem.NavBackend and pass it in; nothing else changes.

The built-in navigation backends are 2D grid pathfinders. In a simulation.dimensions: 3 project, entities can still use 3D positions for replication and collision, but FindPath and NavAgent operate on X/Y waypoints until you provide a custom 3D or navmesh backend.

Terminal window
go get github.com/demiurgos-hub/golem-engine/golem/nav/pathing
import (
"golem"
navpathing "github.com/demiurgos-hub/golem-engine/golem/nav/pathing"
)
layer := tiledMap.LayerByName("Walkable")
nav, err := navpathing.NewFromTiledLayer(
tiledMap.Width, tiledMap.Height,
tiledMap.TileWidth, tiledMap.TileHeight,
layer.Data,
func(gid int) bool { return gid == wallGID },
)
if err != nil {
log.Fatal(err)
}
srv.SetNavBackend(nav)

Same constructor signatures. Swap the import to switch:

Terminal window
go get github.com/demiurgos-hub/golem-engine/golem/nav/kelindar
import (
"golem"
navkelindar "github.com/demiurgos-hub/golem-engine/golem/nav/kelindar"
)
layer := tiledMap.LayerByName("Walkable")
nav, err := navkelindar.NewFromTiledLayer(
tiledMap.Width, tiledMap.Height,
tiledMap.TileWidth, tiledMap.TileHeight,
layer.Data,
func(gid int) bool { return gid == wallGID },
)
if err != nil {
log.Fatal(err)
}
srv.SetNavBackend(nav)

Doors, destructible walls, and triggered barriers change which cells are passable at runtime. Call SetNavWalkable to toggle a cell without rebuilding the grid:

// When a door opens:
srv.SetNavWalkable(door.X, door.Y, true)
// When a door closes:
srv.SetNavWalkable(door.X, door.Y, false)

SetNavWalkable is a no-op if no backend is set or if the backend does not support dynamic updates. Both built-in backends support it.

The pathing backend handles arbitrarily long paths transparently by chaining multiple internal searches. The kelindar backend has no path length limit. Neither requires any path-length management from your code; FindPath always returns the complete route or nil, false if none exists.

See also: Map sources, NavAgent, kelindar backend.