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:

  • 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.
  • 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 golem.nav/pathing
import (
"golem"
navpathing "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 golem.nav/kelindar
import (
"golem"
navkelindar "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.