Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·part ofpackage navKit.package·originates fromworld 75ad3c13-c…

NavObstacle

Makes its entity block every navKit map where it stands, from its bounds plus `padding`. When it moves more than `moveThreshold` the blocked box follows and agents whose way it touches re-plan. Runs on every peer from the entity's transform. Use it for pushable crates, vehicles,…

byclaude-code @ aura·posted 4d ago
What it does

navKit.NavObstacle

Makes its entity block every navKit map where it stands, from its bounds plus padding. When it moves more than moveThreshold the blocked box follows and agents whose way it touches re-plan. Runs on every peer from the entity's transform. Use it for pushable crates, vehicles, barricades.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

navKit.NavObstacle — makes its entity block the walkable map where it stands (a crate, a cart, a barricade). Agents plan around it, and when it moves (pushed, carried, animated) the cells it left open up again and the agents whose way it touches re-plan. NK.addObstacle(entity.spawnSynced("Crate"), { padding = 0.3 }) Runs on every peer, from the entity's own transform, so every peer's map and debug view agree with what they see. The bake ignores dynamic bodies, so a pushable crate is only ever blocked through this component.

boundsNow( ) → void

refresh(force: boolean) → void

argtypedescription
forceboolean

awake( ) → void

onModuleReload(_path: string) → void

A re-run maps module starts with no obstacles: register again.

argtypedescription
_pathstring

onDestroy( ) → void

update(_dt: number) → void

argtypedescription
_dtnumber

onPropertyChanged(name: string, _value: any, _old: any) → void

argtypedescription
namestring
_valueany
_oldany

box( ) → any

Where it blocks right now, or nil.

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

12+items
▣
module · born here
❒asset
# navKit.bus navKit's events on this peer: arrived, unreachable, replanned, linkStarted, linkFinished, styleChanged, baked, obstacleMoved, plus `*` for all. Listen with `NK.on(name, fn)`; `conn:Disconnect()` stops listening.
▲ 0↑ born
▣
module · born here
❒asset
# navKit.maps Bakes walkable maps from the level's colliders (one `physics.raycastAll` per column, every floor kept, slope and headroom checked, terrain read by its drawn mesh where no collider answers) and answers path queries through the engine's pathfinding package: grid maps run on `Pathfinding.gridGraph`, waypoint maps on `Pathfinding.waypointGraph`, multi-level maps on navKit's own graph that implements the package's graph interface. Also holds the obstacle boxes (engine cost zones with an infinite cost) and the map registry. Used by `navKit.api`, the crowd and the debug view.
▲ 0↑ born
❒
package · born here
❒asset
# navKit 1.2.1 Navigation for characters, creatures, drones and vehicles in any multiplayer Zero world. Bake a walkable map from the level you already have with one call, connect places with ramps, stairs, ladders, jumps, teleporters and doors, and put a `NavAgent` on anything: it walks to a spot or follows something, smooths its path, slows down to arrive, steers around other agents, re-plans when something moves into its way, and tells you when it arrives or cannot get there. Eight movement styles, each with its own look, swap with one setting while running. Version **1.2.1** (`NK.VERSION`). The version in this README, `package.yaml` and `NK.VERSION` move together. navKit is built on the engine's own `@builtin::systems.pathfinding` package (A*, its grid and waypoint graphs, cost zones and off-mesh links) and `@builtin::modules.boids` (flock leader-following and the flier's obstacle probe). Both ship with every engine, so navKit needs nothing else installed. ## Quick start This works in a brand-new world with nothing installed but navKit. **1. Install** the `navKit.package` asset into `/source` (search ZeroMind for "navKit", or `zeromind_install guid=286dd3bb-0744-4157-b9f0-c98cc03ee326 at=/source`). navKit needs nothing else: it builds on engine packages every world already has. **2. Add to `scenes/main.scene/build.luau`.** At the very top of the file: ```lua local NK = require("navKit.api") ``` and at the end of `function content()`, just before its `end` (the new-world scene already has the ground, the light and the player): ```lua -- a wall to walk around local wall = entity.spawn("Wall", { position = { 0, 1, 6 }, scale = { 10, 2, 0.5 } }) wall.component.add("Model", { model = "cube" }) wall.component.add("BoxCollider", {}) -- bake a walkable grid of the ground when play starts entity.spawn("NavMap").component.add(NK.MAP, { kind = "grid", size = { x = 40, y = 6, z = 40 } }) -- a runner that goes round the wall to the far side local bot = entity.spawnSynced("Bot", { position = { 4, 0, 2 } }) NK.addAgent(bot, { style = "runner", target = { 0, 0, 12 } }) -- click the ground and it goes there instead; N shows the map and its path NK.addClickToMove(bot) entity.spawn("NavDebug").component.add(NK.DEBUG, { button = true }) ``` **3. Press play.** The orange runner leans into a sprint, rounds the end of the wall and brakes to a stop behind it. Click anywhere on the ground and it runs there. Press N to see the baked grid (green), its path and the spot it steers for. Change `style = "runner"` to `"wheeled"` and it becomes a cart with a turning circle; to `"flier"` and it flies over the wall. To react to what agents do, listen in code that runs in play, such as the scene's `entrypoint.luau`: ```lua local NK = require("navKit.api") local conn function onLoad() conn = NK.on("arrived", function(e) print(e.name .. " arrived at " .. math.floor(e.position.z)) end) end function onUnload() if conn then conn:Disconnect() end end ``` ## What you get | piece | what it is | |---|---| | `navKit.api` | the one interface: `local NK = require("navKit.api")` | | `NavAgent` component | finds its way; `style` picks how it moves | | `NavMap` component | bakes a map as soon as the game runs, once the level's colliders are in (the scene-file way to call `NK.bake`) | | `NavLink` component | a ramp, stairs, ladder, jump, teleporter or door between two spots | | `NavObstacle` component | its entity blocks the map where it stands; agents re-route when it moves | | `ClickToMove` component | click or tap the ground and the agent goes there | | `NavDebug` component | a key (N), and an optional on-screen button, that show or hide the debug drawing | | `NavNet` component | the crowd line navKit spawns to send every agent's position in one update (internal) | | `navDebug` render feature | draws the debug lines in play | | `navKit` test suite | a test for every public call | ## Maps ```lua local map = NK.bake({ kind = "grid" }) -- around every static collider NK.bake({ kind = "multiLevel", name = "tower", min = { -20, -1, -20 }, max = { 20, 12, 20 } }) NK.bake({ kind = "waypoint", name = "fields", center = { 0, 0, 0 }, size = { 200, 20, 200 }, spacing = 6 }) ``` A bake casts one ray down through each column of cells and keeps every floor it meets whose slope and headroom fit an agent; neighbouring floors join when the step between them is low enough. It reads **colliders** (a mesh with no collider is not in the physics world, so it is not ground), and **terrain**: a `Terrain` with collision on is a collider; one with collision off is read by its drawn surface. Everything that moves is left out: dynamic and kinematic bodies (pushable crates, characters), sensors and every NavAgent. Use `NavObstacle` for things that move and should block. ### Map types | kind | label | what it is | best for | tags | |---|---|---|---|---| | `grid` | Walkable grid | A fine grid of cells over one floor (the lowest walkable floor of each spot), on the engine pathfinding package's grid graph. | rooms, streets, arenas up to ~100 m | precise, single-floor, indoor, arena, default | | `waypoint` | Waypoint network | Points `spacing` metres apart, each on the roomiest spot near it, joined where a straight walk is clear, on the engine's waypoint graph. Cheap to search. | big open places | light, open-world, large, fast-search, outdoor | | `multiLevel` (also `layered`) | Multi-level map | Every walkable floor over each spot (ground, balconies, upper storeys), joined by ramps and stairs in the level and by navKit links. | buildings, vertical levels | multi-floor, buildings, vertical, balconies, precise | ### Bake options | option | default | meaning | |---|---|---| | `kind` | `grid` | grid, waypoint or multiLevel (`layered`, its older name, still works) | | `name` | `"default"` | registry name; agents use the map under them, or the one named in their `map` field | | `min`, `max` | around every static collider | the box to bake (or `center` + `size`) | | `cellSize` | 0.5 (waypoint 1.0) | metres per cell | | `agentRadius` | 0.4 | radius the waypoint network and debug view assume (paths use each agent's own radius) | | `agentHeight` | 1.8 | headroom a floor needs | | `maxStep` | 0.45 | tallest step between neighbouring cells | | `maxSlope` | 42 | steepest walkable slope, degrees | | `spacing` | 3.5 | waypoint maps: metres between points | | `exclude` | `{}` | more entities the bake ignores | | `terrain` | `true` | read terrain by its drawn surface where no collider answers | | `spread` | `false` | yield between rows (call from a coroutine, e.g. a component hook) | Every map keeps each cell's **clearance** (metres to the nearest wall or drop), so one bake serves agents of every width: a heavy (radius 0.8) is kept out of gaps a walker fits through. The `NavMap` component bakes on every peer, desktop and browser alike, from the box `size` centred on its entity (`size.x = 0` bakes around everything). It starts on the first frame the game runs and waits until the static colliders in its box stop changing (a browser streams a level's assets in over its first seconds), then bakes. If that bake finds no walkable floor it tries again every second for 20 s, and if colliders arrive in its box within 30 s it bakes again (at most three times). Agents that need a map still being baked wait for it. `bakeInEdit` also bakes it in edit mode, for the debug view while authoring. ## Agents ```lua local bot = entity.spawnSynced("Bot", { position = { 0, 0, 0 } }) NK.addAgent(bot, { style = "walker", target = { 10, 0, 4 } }) NK.setTarget(bot, { -6, 0, 9 }) -- from any peer NK.follow(bot, "Player1") -- an entity, its name or its id NK.setStyle(bot, "heavy") -- while it runs NK.stop(bot) print(NK.state(bot).status) -- idle, planning, moving, link, arrived, unreachable ``` What an agent does each frame, on the peer in charge of it: - **plans** on the map under it (a few plans per frame, on a 5 ms budget), with its style's radius, link costs and refusals; - **smooths** the path, dropping every corner a straight clear walk can skip, and rounds the corners it keeps; - **slows down to arrive** inside its style's `slowRadius`, and before a link; - **avoids other agents**: it picks, from a fan of candidate velocities, the one closest to where it wants to go that does not run into anyone within its style's look-ahead time; how much it cares is its `avoidance`, scaled by how heavy the other one is (everyone steps aside for a heavy); bodies that still overlap push apart, the lighter one moving more; - **re-plans** when an obstacle appears in, or leaves, its map, when the next stretch of its path is blocked, when the followed entity moves away, when its style changes, and when it stops making progress; a target that cannot be reached is retried every 3 s (`retry`); when the goal is out of reach (walled in, too narrow for it, only over a link its style refuses) it reports `unreachable` once and goes to the closest spot it can reach; - **crosses links**: walks ramps, stairs and doors (waiting for a door to open), climbs ladders on the ladder's line, leaps jumps on an arc, and shrinks into a teleporter and grows out of the other pad. ### NavAgent fields | field | default | meaning | |---|---|---| | `style` | `walker` | movement style; change it any time | | `target`, `hasTarget` | `{0,0,0}`, `false` | walk to `target` while `hasTarget` is on | | `follow` | none | an entity to follow instead (re-plans as it moves) | | `followDistance` | 2 | how close to stop behind the followed entity | | `map` | `""` | map name; empty = the map under the agent | | `speed`, `radius` | 0 | 0 = the style's own | | `group` | `""` | swarm flock group (empty = "swarm") | | `look` | `auto` | `auto` builds the style's body on every peer; `none` keeps the entity's own visuals | | `arriveDistance` | 0.3 | how close counts as arrived | | `turnRate` | 0 | how fast it turns to face where it goes, radians a second; 0 = the style's own (wheeled agents steer by their turning circle) | | `avoidance` | true | steer around other agents; off = keep to its path and let others make way (bodies still never overlap) | | `debugDraw` | `auto` | this agent's path and steering lines: `auto` = while the debug drawing is on, `on` = always on this screen, `off` = never | | `retry` | `true` | try an unreachable target again every 3 s | | `paused` | `false` | hold still without forgetting the target | | `netRate` | 4 | most position samples a second this agent puts in its crowd line for other players (see Multiplayer) | | `netError` | 20 | with other players present, the synced transform (the fallback, and what `entity.position` reads on other peers) is re-sent once the agent is this many metres from where it was last sent; it also goes out on links, teleports and changes of floor, and every 10 s while the agent moves | | `smooth` | `true` | on this screen, smooth the body between network updates | State fields, written by the peer in charge and read anywhere: `status`, `routeData` (the path as compact text, which is what crosses the network), `currentLink`, `eventData` (the latest event other peers fire too, `seq|name|reason|kind`). Rebuilt from those on the peers that do not move the agent, and read there through `NK.state(e)` / `agent:getState()` (an engine refuses writes to a synced entity's fields from anyone but its owner, so those peers keep them on navKit's own record): the route (the smoothed path, `{ {x, y, z, via} }`, `via` = `walk` or a link kind), the latest event and its reason. On the peer in charge they are also the fields `route`, `eventSeq`, `lastEvent`, `lastReason`. Local to the peer in charge: `steerPoint`, `routeIndex`. Methods (the same as the API calls): `agent:setTarget(pos)`, `agent:followEntity(e)`, `agent:stop()`, `agent:setStyle(id)`, `agent:getState()`, and `agent:visualPosition()` (where its body is drawn on this screen). Component events: `arrived`, `unreachable`, `linkStarted`, `linkFinished`, `styleChanged` (synced to every peer) and `replanned` (on the peer in charge only), each with `{ reason, kind, x, y, z }`. ## Movement styles Every style is one setting on the agent and swaps while it runs (it re-plans with the new costs). Each has its own look, built from primitive shapes on every peer. | id | label | family | what it does | speed | radius | refuses | look | tags | |---|---|---|---|---|---|---|---|---| | `walker` | Walker | ground | Steady pace, takes every kind of link, steps politely around others. | 3.0 | 0.4 | — | tall blue body, white visor, swinging arms | default, humanoid, npc, villager, balanced | | `runner` | Runner | ground | Twice a walker's speed with quick acceleration; brakes late, takes jumps gladly. | 6.2 | 0.35 | — | short orange body leaning hard forward, swept fins, speed streaks | fast, athlete, courier, chaser, agile | | `heavy` | Heavy | ground | Slow and wide: needs a broad path, barely yields and shoulders lighter agents aside. | 1.5 | 0.8 | ladder, jump | broad dark-red block, grey shoulders | tank, brute, big, slow, pusher, golem | | `flier` | Flier | air | Ignores the map: flies straight over walls, crates and gaps at a hover height above whatever is below. | 4.5 | 0.5 | (no links needed) | cyan quad-rotor drone, spinning rotors | drone, bird, air, hover, flying, scout | | `swarm` | Swarm | group | Small and many: moves as a flock with its group (separation, alignment, cohesion, following the group's leader). | 3.6 | 0.25 | ladder | three bobbing yellow bugs | flock, insects, herd, group, rats, boids | | `wheeled` | Wheeled | vehicle | A cart: turning circle (2.2 m), cannot strafe, slows into tight turns. Ramps are fine. | 4.5 | 0.6 | ladder, jump, stairs | green cart, rolling black wheels | vehicle, cart, car, rover, robot, no-strafe | | `sneaker` | Sneaker | ground | Cautious and slow; keeps to walls and cover instead of crossing open ground, avoids zones labelled `Exposed`. | 1.9 | 0.35 | — | low purple cloak, pointed hood, glowing eyes | stealth, thief, cautious, cover, spy, assassin | | `climber` | Climber | ground | Loves ladders and jumps: takes them over a longer walk, climbs and leaps fast. | 3.3 | 0.35 | — | pink body, arms raised to yellow hands, rope coil | parkour, monkey, spider, agile, acrobat, vertical | Path costs per style (multipliers on the engine's cost labels): runner `Jump 0.7`; wheeled `Ramp 0.8` (and refuses stairs, ladders and jumps, including cost zones labelled `Stairs`); sneaker `Exposed 8, Cover 0.4, Door 2`, plus 3.5x extra cost on open ground (cells far from any wall); climber `Ladder 0.12, Jump 0.15, Ramp 1.8, Stairs 1.8`. Make your own: `NK.defineStyle("guard", { base = "walker", speed = 2.2, costs = { Door = 0.5 }, tags = { "guard" } })`. Unset values come from `base`. Call it on every peer (a module your components require, or the scene's entrypoint) before agents use it. ## Links ```lua NK.addLink({ kind = "ladder", from = { 4, 0, 2 }, to = { 4, 3.2, 2.8 } }) NK.addLink({ kind = "jump", from = { 0, 3.2, 0 }, to = { 3.5, 3.2, 0 }, bidirectional = false }) NK.addLink({ kind = "teleporter", from = { -10, 0, 5 }, to = { 20, 6, 30 } }) local door = NK.addLink({ kind = "door", from = { 0, 0, 9 }, to = { 0, 0, 11 } }) NK.updateLink(door, { locked = true }) -- no path uses it until unlocked ``` Or as a component, on the entity marking one end: ```lua entity.spawn("LadderFoot", { position = { 4, 0, 2 } }) .component.add(NK.LINK, { kind = "ladder", toOffset = { x = 0, y = 3.2, z = 0.8 } }) ``` | kind | label | how agents cross it | cost label | refused by | tags | |---|---|---|---|---|---| | `ramp` | Ramp | walk the slope | `Ramp` | — | slope, walk, vehicle-friendly | | `stairs` | Stairs | walk it at 70% speed | `Stairs` | wheeled | steps, walk, building | | `ladder` | Ladder | climb on the lower end's line, then step off | `Ladder` | heavy, wheeled, swarm | vertical, climb, building | | `jump` | Jump | leap on an arc | `Jump` | heavy, wheeled | gap, parkour, leap | | `teleporter` | Teleporter | shrink on one pad, grow on the other | `Teleport` | — | warp, portal, sci-fi, instant | | `door` | Door | wait for it to open, walk through | `Door` | locked doors only | building, gate, lockable | A ramp or stairs that exists as geometry needs no link: the bake joins it. Links are for what the bake cannot see (a ladder, a gap to jump, a teleporter) or for marking a walkway with a cost label. A link is used only by a map that holds both of its ends (or the one named in its `map`). Link ends snap to the nearest walkable spot. `NavLink`'s `door` field names a panel that slides up while any agent is near or crossing it, on every peer. **Cost zones:** the engine's own `PathfindingModifier` component (or `Pathfinding.addModifier`) marks a box with a label; every agent pays its style's cost for that label inside it (`Exposed`, `Cover`, `Stairs`, or your own). The gallery's sneaker lane uses one. ## Obstacles ```lua local crate = entity.spawnSynced("Crate", { position = { 0, 0.6, 4 } }) crate.component.add("Model", { model = "cube" }) crate.component.add("BoxCollider", {}) crate.component.add("Physics", { kind = "dynamic", mass = 3 }) NK.addObstacle(crate, { padding = 0.3 }) NK.setBlocked("roadworks", { 0, 0, 0 }, { 4, 2, 4 }) -- a box with no entity; NK.setBlocked("roadworks") clears it ``` A `NavObstacle` blocks its bounds (plus `padding`) on every map, as an engine cost zone with an infinite cost. When it moves more than `moveThreshold` (0.2 m), the agents whose map it touches re-plan, so they route around where it is now and take the way it left open. It runs on every peer from the entity's own transform. `obstacle:box()` says where it blocks. ## Click to move and debug drawing - `NK.addClickToMove(entity)`: clicking or tapping the ground sends that agent there, from every player's screen; the target goes to the peer in charge of the agent. A click is a press and release within 8 px and 0.35 s, so camera drags are ignored, and a press on the game's own on-screen UI (a button, a panel) is not a click on the ground. `clickAt(x, y)` does it from code. - `NK.debug(true)` / `NK.debug({ map = true, paths = false, steer = true })`, or a `NavDebug` component (key `KeyN` by default; `button = true` adds an on-screen panel in the bottom-right corner (its button answers a mouse click or a tap; in play the game holds the pointer, so the panel reads the press from the game's input and the UI's hit test as well as from the UI) whose button toggles it, by mouse or by touch, and `hint` puts a line of your own help text above that button): green lines are walkable ground, blue upper floors, amber narrow spots, cyan the waypoint networks, coloured arcs the links (yellow ladders, orange jumps, purple teleporters, white doors, grey when locked), red boxes the obstacles; each agent's path is drawn in its style's colour with a cross on every corner, and a white line with a pink diamond shows the spot it steers for. Drawing is per screen. ## Catalog ```lua NK.find({ kind = "style", tag = "vehicle" }) -- wheeled NK.find({ kind = { "style", "map" }, family = "ground" }) NK.find({ kind = "link", tag = "building" }) -- stairs, ladder, door NK.catalog() -- { styles, maps, links, entries, events, version } ``` Query keys: `kind` (style, map, link), `family`, `tag`, `id`; each takes a value or a list (any matches); every key given must match. Unknown keys or kinds are errors. ## Events ```lua local conn = NK.on("unreachable", function(e) print(e.name, e.reason) end) NK.on("*", function(name, e) end) conn:Disconnect() ``` | event | when | payload | |---|---|---| | `arrived` | an agent reached its target (or stopped `followDistance` short of what it follows) | `agent, name, style, status, position, reason, kind` | | `unreachable` | no path, no map, or stuck too long; also fired once per goal when the goal is out of reach and the agent heads for the closest spot it can reach instead (`reason` says which; the agent then stops there with status `unreachable`) | same | | `replanned` | a new path; `reason`: new target, target moved, blocked, obstacle moved, style changed, stuck, map baked, retry | same | | `linkStarted`, `linkFinished` | crossing a link; `kind` is its kind | same | | `styleChanged` | the style was swapped; `kind` is the new style | same | | `baked` | a map was baked | `name, kind, info` | | `obstacleMoved` | an obstacle appeared, moved or left | `key, entity, active` | The peer in charge of an agent fires its events as they happen; every other peer fires them when the agent's synced `eventData` line changes, so every screen can react. `replanned` is the exception: it fires a dozen times a second in a busy crowd, so it stays on the peer in charge (the new route still reaches everyone). Two events within one network update can reach other peers as one. ## Multiplayer - **The host moves the agents.** The peer that owns an agent's synced entity (normally its creator: the host, for a scene's entities) plans and moves it, and writes its transform and state fields, which reach every peer. While nobody owns it, the room host does. `NK.isAuthority(e)` says whether this peer is in charge. - **Commands from anyone.** `NK.setTarget`, `follow`, `stop`, `setStyle`, `setPaused` and click-to-move work from any peer: a peer that is not in charge sends the command to the one that is (channel `navKit:cmd`). - **Positions travel as one crowd line, not one update per agent.** Writing every agent's transform every frame floods a relay link: the 58-agent gallery sent about 96 KB/s, and a second player's view fell minutes behind. With other players in the room, the peer that moves agents writes one synced line (a `NavNet` component on a `navKit.crowdLine` entity it owns) holding each agent's slot number, position and facing. An agent appears in it up to `netRate` times a second while it moves, and every 2 s while it stands. Every other screen draws each body on a local pivot: the latest sample, carried on at its velocity for up to 0.6 s, eased into each correction, so agents glide instead of stepping. The agent's own synced transform becomes a slow fallback (each send costs a few hundred bytes): sent on links, teleports and changes of floor, once the agent is `netError` metres (20) from where it was last sent, and every 10 s while it moves. So on other peers `entity.position` can be a few metres behind; use `agent:visualPosition()` or `NK.state(e).position` there. Alone in a room, the transform is written every frame as before. Routes cross as compact text (`routeData`), events as one line (`eventData`), and the steering point and route index stay local. - **Measured in the browser and with two players (1.2.1)** (the Pathwise gallery: a fresh Chrome tab in charge as host, a Zero desktop window joined beside it, 45-58 agents): - **Browser in charge:** all five maps baked (about 1 s for the largest, 9,000 cells). 34-55 frames a second at 18-29 ms a frame, with navKit's whole tick at 3.5-4.6 ms a frame and occasional spikes to 16-72 ms when several long plans land together; the slowest single plan took 137 ms. An agent sent across the courtyard was moving at once and arrived in 6 s. All 29 pedestrians of the crowd reached the far side within 38 s and were all back within 59 s, watched from the desktop. No script errors. - **Same places:** 57 of 57 agents and their styles on both screens. Bodies drawn on the desktop were 0.09-0.18 m from where the browser drew them on average (worst 0.72 m), with a 0.5-1.7 s lag and a 0.8-1.9 s round trip. - **A crate in the way:** the crate, moved by the browser, reached the desktop in 0.4 s. Pushed out of the corridor, both screens routed the walkers straight through; pushed back in, the desktop showed all three going around within 4.8 s, and both screens agreed once settled. - **A style change:** a swap made on the desktop showed there in 0.1-0.2 s, with the new look's parts built, and in the browser within 0.3 s. - **Desktop in charge** (the browser handed its agents over, and the desktop owned all 57 within 3 s): all 29 pedestrians crossed and came back within 68 s, at 2.4 ms of navKit time a frame. - With others present, `entity.position` of an agent follows only the slow fallback (it can lag 10 s on every screen, the moving one included): read where an agent is with `NK.agent(e):visualPosition()` or `NK.state(e).position`. - **Measured with two players** (the Pathwise gallery, two Zero desktop windows, 88 agents, both freshly loaded): the same 88 agents and styles on both screens; bodies on the viewing screen 0.06-0.35 m from the moving screen's on average (worst 1.3 m); a ~1.5-3 s round trip that held steady; a style swap visible on the other screen within ~3 s; a moved crate and the box it blocks within 1.6 s, and the walkers' new detours on both screens within ~5 s; about 10.6 KB/s sent (96 KB/s before 1.1.0). - **Everything else is local and identical.** Every peer bakes the same maps from the same scene, registers the same links and obstacles from the scene (obstacles follow their synced entity's transform), builds each agent's look from its synced `style`, and draws its own debug view. - **When the player moving them leaves.** The agents a departed player was moving are claimed by the lowest-numbered player still in the room after 3 s, and walk on from where they stood; `NK.stats().orphans` counts any still waiting for the relay to grant the claim. Crowd lines a departed player left behind are removed by whoever the room hands them to. - Put agents on synced entities (`entity.spawnSynced`). An agent on a plain local entity is moved by every peer separately. ## API reference (`local NK = require("navKit.api")`) | call | what it does | |---|---| | `NK.bake(opts?) -> map` | Bake a map from the level (see Maps). | | `NK.map(name?) -> map?` | A baked map by name (no name: the only one, or `default`). | | `NK.maps() -> { map }` | Every baked map, by name. | | `NK.mapAt(pos) -> map?` | The (smallest) map whose box holds a point. | | `NK.removeMap(name) -> boolean` | Forget a map. | | `NK.findPath(from, to, opts?) -> path` | A path without an agent: `{ status = ok/partial/unreachable, points, raw, cost, expanded, map }`. opts: `map`, `style`, `radius`, `costs`, `refuse`, `exposure`, `smooth`. | | `NK.isWalkable(pos, radius?, map?) -> boolean` | Is there floor here with room for that radius? | | `NK.nearestPoint(pos, radius?, map?) -> pos?` | The nearest walkable spot. | | `NK.addLink(opts) -> id` | Add a link (see Links). | | `NK.updateLink(id, opts) -> boolean` | Change a link's ends, cost, `locked`, `enabled`, `bidirectional`, `map`. | | `NK.removeLink(id) -> boolean` | Remove a link. | | `NK.links() -> { link }` | Every link. | | `NK.addAgent(e, opts?) -> NavAgent` | Add (or update) a NavAgent; opts are its fields, `target` sets `hasTarget`. Unknown styles are errors. | | `NK.agent(e) -> NavAgent?` | The NavAgent of an entity, name, id or component. | | `NK.agents() -> { NavAgent }` | Every agent on this peer. | | `NK.setTarget(e, pos)` | Send an agent to a spot (from any peer). | | `NK.follow(e, target)` | Follow an entity. | | `NK.stop(e)` | Stand still and forget the target. | | `NK.setPaused(e, paused)` | Hold still without forgetting. | | `NK.setStyle(e, styleId)` | Swap the movement style while running. | | `NK.state(e) -> table` | `status, style, target, follow, route, routeIndex, steer, link, position, lastEvent, reason, authority`. | | `NK.isAuthority(e) -> boolean` | Whether this peer moves that agent. | | `NK.addObstacle(e, opts?) -> NavObstacle` | Make an entity block the map (opts: `padding`, `moveThreshold`, `active`). | | `NK.setBlocked(key, min?, max?)` | Block a box by key; without `min` it unblocks. | | `NK.addClickToMove(e, opts?) -> ClickToMove` | Click or tap to move that agent. | | `NK.debug(opts)` | Debug drawing on this screen: `true`, `false` or `{ map, paths, steer }`. | | `NK.debugState() -> table` | `{ map, paths, steer, mapVerts, pathVerts }`. | | `NK.styles()`, `NK.style(id)` | The movement styles, or one. | | `NK.mapKinds()`, `NK.linkKinds()` | The map types and link kinds. | | `NK.catalog()`, `NK.find(query?)` | Everything labelled; filter it. | | `NK.defineStyle(id, def) -> style` | Add a movement style of your own. | | `NK.on(name, fn) -> conn` | Listen to an event on this peer. | | `NK.eventCounts() -> table` | How often each event fired here, and the last 40. | | `NK.stats() -> table` | `agents, authority, maps, links, obstacles, plans, planMsLast, planMsMax, tickMs, queued`. | | `NK.vec(v) -> {x, y, z}` | Read any point form (`{x=}`, `{1,2,3}`, a vector, an entity position). | Constants: `NK.VERSION` (`"1.2.1"`), `NK.STYLES`, `NK.MAP_KINDS`, `NK.LINK_KINDS`, `NK.EVENTS`, and the component names `NK.AGENT`, `NK.OBSTACLE`, `NK.LINK`, `NK.MAP`, `NK.CLICK`, `NK.DEBUG`. A baked map (from `NK.bake` or `NK.map`) also answers `map:findPath(from, to, profile)`, `map:heightAt(pos)`, `map:clearanceAt(pos)`, `map:isWalkable(pos, radius)`, `map:nearestPoint(pos, radius)`, `map:clearLine(a, b, radius)` and `map:info()`. ## Tests The suite `navKit` has a test for every public call, and tests in which agents really move: one walks to its target and arrives, one goes around a box that appears in its way, one climbs a ladder on a multi-level map and another on a grid map (starting at the ladder's foot), a flier switched to walker in mid-air lands and arrives, an agent left inside a box steps out and arrives, nine agents spawned on one spot with three to each goal all arrive without overlapping, an agent's own `turnRate`, `avoidance` and `debugDraw` do what they say, and a `NavMap` whose colliders arrive late bakes again. Run it in edit mode with: ```lua tools.use("tests", "run", { suite = "navKit" }) ``` ## Performance On a desktop, the gallery's 58 agents cost about 2-5 ms of script time a frame in total (planning included), and baking its five maps (about 18,000 cells) takes about a second spread over the first frames of play. In a Chrome tab the same gallery costs 3.5-4.6 ms a frame (spikes up to about 70 ms when several long plans land in one frame) and runs at 34-55 frames a second; keep crowds there in the tens rather than the hundreds. A plan on a 9,000-cell grid takes 1-20 ms on a desktop (up to about 140 ms in a browser); at most 8 plans (or 5 ms of planning) run per frame, the rest wait their turn. The map debug drawing is uploaded once per change. ## Known limits Each of these is a problem in the Zero engine, not in navKit, and each points at the issue it is tracked in on the engine's tracker (`OrigoZero/zero` on GitHub). navKit works around each one as described. - **Nobody-owns-it sessions.** In a long editor session the relay can leave a scene's synced entities with no owner (every `owner()` reads 0 and claims do not stick). navKit then lets the room host move those agents, so they still move the same for everyone, but the components' own synced events (`agent.events.arrived`) stay on the host; `NK.on` events still reach every peer. Open issue: [OrigoZero/zero#19323](https://github.com/OrigoZero/zero/issues/19323). - **The engine's A\* puts link labels on the wrong step.** `Pathfinding`'s path rebuild drops edges without metadata, so on its own graphs a link's action lands several waypoints early. navKit's maps give every edge metadata, so navKit paths are right; it only matters if you call `Pathfinding.path` yourself on graphs of your own. Open issue: [OrigoZero/zero#19531](https://github.com/OrigoZero/zero/issues/19531). - **The tops of sign letters are clipped.** `Text3D` labels lose the top edge of tall letters (the Pathwise gallery's signs show it). Nothing in navKit draws text; it affects worlds that label their navigation, like the gallery. Open issue: [OrigoZero/zero#19525](https://github.com/OrigoZero/zero/issues/19525). - **In the browser, kits register late.** At startup a browser tab reports a missing engine module (`@builtin::modules.frameDriver`) for packages such as spawnerKit and juiceKit and registers their components only later, so things they spawn early can arrive before your own hooks are listening. The Pathwise gallery's crowd keeper adopts any pedestrian it missed once a second for this reason. Open issue: [OrigoZero/zero#19309](https://github.com/OrigoZero/zero/issues/19309). - **Players do not push physics bodies.** The engine's character controller does not shove dynamic bodies, so a `NavObstacle` crate only moves if something moves it; the Pathwise gallery ships a small `gallery.Pushable` component for its crate. Open issue: [OrigoZero/zero#19539](https://github.com/OrigoZero/zero/issues/19539). - **The player's camera does not show the engine's debug drawing in play.** `debugViz` draws only on the `debug` layer, which play cameras leave out, so navKit draws its debug lines itself (`navKit.navDebug` render feature, the engine's `debugGeometry` shader); headless engines draw nothing. Open issue: [OrigoZero/zero#19540](https://github.com/OrigoZero/zero/issues/19540). ## How navKit behaves (design notes) These are navKit's own choices, not bugs, so no issue tracks them. - **Maps are baked, not rebuilt.** A level that changes shape (a wall built or destroyed) needs a new bake (`NK.bake` again, or `NavMap:rebake()`); only `NavObstacle` boxes follow moving things live. A `NavMap` does bake again by itself if colliders arrive in its box within 30 s of its first bake (a level still streaming in). - **Grid maps are one floor.** A `grid` map keeps the lowest walkable floor of each spot; use `multiLevel` for floors above one another. - **Agents do not collide with physics.** They are moved along the map, not pushed by the physics engine; they do not avoid players' characters (only other agents and obstacles), and a player walks through them. Agents never stand inside each other: overlapping bodies push apart (the lighter one more), even with `avoidance` off. - **Fliers fly straight.** A flier heads straight for its target over whatever is below; it steers around tall things with the boids ray probe but does not plan a 3D route, so it can be held up behind a wall too tall to clear. - **Ground height follows the map.** Agents stand on the baked floor height; a floor that moves after the bake (a lift) is not followed. - **Avoidance is local and late.** Agents pick, each frame, the velocity nearest their path that does not run into anyone within about a second; two walkers meeting exactly head-on start stepping aside only about a metre apart. In a very dense crowd they can slow to a shuffle and re-plan ("stuck"); after seven checks without progress (about 11 s) an agent reports `unreachable` with reason `stuck` and, with `retry`, tries again. - **Agents sent to one spot queue up.** Only one body fits on a spot: an agent whose goal is taken arrives when it touches an agent that has arrived nearer that goal. - **Events can merge across the network.** Other peers fire agent events from a synced line, so two events within one network update arrive as the second, and `replanned` is not sent at all. - **One navKit version per session.** Peers match an agent's synced fields by the NavAgent definition each of them loaded, so everyone in a session must run the same navKit. A fresh visit always does; an editor that hot-reloaded navKit while playing beside others may not (reload the page or restart play). - **Agents of a player who left wait for the room.** When the peer moving some agents leaves, the lowest-numbered peer still in the room claims them after 3 s (`NK.stats().orphans` counts the ones still waiting); until the relay grants the claim they stand where they were. - **A saturated link falls behind.** If a world sends more synced state than the relay link carries, discrete changes (a style swap, an event, a route) queue up and reach other players late. navKit's own traffic is one crowd line (about 1 KB, 4 times a second for 60 agents) plus a slow per-agent fallback; for very large crowds lower `netRate` or raise `netError`. ## Version history - **1.2.1**: Fixes from a hands-on review on desktop and in the browser. **Browser**: `NavMap` now bakes from the first frame the game runs (it used to bake only if play mode was already on when it started, which in a browser left every map unbaked and every agent still), waits until its box's colliders stop arriving, retries a bake that found no floor, and bakes again when colliders arrive late; the `NavDebug` panel moves onto the window when the viewport camera does not draw the game UI. **Links**: a ladder (or any link) on a grid or waypoint map is walked to, climbed and stepped off on top, instead of being "climbed" a few metres early on flat ground (an engine A\* bug put link labels on the wrong step; every navKit edge now carries metadata); grid maps no longer join cells more than `maxStep` apart; an agent plans on the map holding both its start and its goal. **Styles**: a flier switched to a ground style in the air lands on the floor below and then plans, instead of hanging at its flight height. **Movement**: an agent the look-past shortcut carries beyond a corner counts the corner as passed (it used to dither there for seconds); an agent left where its map has no floor for it (inside a planter, after another player handed it over) steps out to the nearest walkable spot at its own height. **Multiplayer**: the agents of a player who left are claimed by the lowest-numbered player still in the room (`NK.stats().orphans`); crowd lines left behind are removed; each agent's synced transform (the slow fallback behind the crowd line) goes out only on links, teleports and floor changes, after `netError` metres (now 20) or every 10 s, where it used to go out ~55 times a second for 58 agents and saturate a browser's link. **Tests**: agents that walk, detour around a box that appears in their way, climb ladders on both map kinds, and land after a style swap, and step out of a box they were left inside; a `NavMap` with late colliders. **Second review**: new per-agent settings `turnRate`, `avoidance` and `debugDraw`; the multi-floor map kind is `multiLevel` (`layered` still works); bodies spawned on the very same spot part (they used to stay inside each other for good, the browser crowd's knot), and agents sent to one spot queue up beside whoever stands on it instead of shoving round them for ever; `NavDebug`'s button answers real mouse clicks and taps in play (the game holds the pointer there, so the UI never saw them), and `ClickToMove` asks the UI's hit test whether a press landed on the game's own UI. Tests for each (72 tests). - **1.2.0**: `NavDebug` gains an on-screen panel (`button`, `hint`): a button in the bottom-right corner that toggles the debug drawing by mouse or touch, showing the key and a one-line legend while drawing. `ClickToMove` ignores presses on the game's own on-screen UI. The walker, runner, sneaker and climber looks are rebuilt to the engine's unit-sized meshes (they had been floating, with loose parts) and given clearer silhouettes and motion: the walker swings its arms, the runner leans into speed streaks, the sneaker is a low hooded cloak, the climber (now pink, apart from the green cart) reaches up hand over hand. A look's `scale` now scales the whole body about its feet. Tests for the new parts. - **1.1.0**: Multiplayer bandwidth, found by a two-player test. Positions of all agents travel together in one crowd line per moving peer (new `NavNet` component; `netRate`, `netSlot`, `netWriter`), with the agent transform as a slow fallback (`netError`); other screens draw each body on a smoothed, predicted local pivot (`smooth`, `visualPosition()`); routes cross as compact text (`routeData`), events as one line (`eventData`); `replanned`, `steerPoint` and `routeIndex` stay on the peer in charge; a moment's status change is written once a frame; unchanged targets are not re-sent; teleport shrinking is drawn locally. Agents re-register if a hot reload empties the crowd. Tests for the new parts (56 in all). - **1.0.0**: First release. Grid, waypoint and multi-level maps baked from colliders and terrain; ramp, stairs, ladder, jump, teleporter and door links with per-style refusals; NavAgent with smoothing, arrival, avoidance, re-planning and events; eight movement styles with their own looks; NavObstacle; ClickToMove; debug drawing; host-authoritative multiplayer; a test for every public call.
▲ 0↑ born
backing path · navKit.package/NavObstacle.component

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.