Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Movement and navigation

Creatures move with the players’ integrator and find their way through a world players are editing: they know cheaply whether they can reach a target, search within a bounded budget on worker threads, follow a corridor, and repair it locally when a block changes under it. The design and its rationale are in Talos C (docs/superpowers/specs/2026-09-27-talos-c-movement-navigation-design.md); this page is the current contract for what phases C0, C1, C2 (terrain breadth), C3 (crowds: shared flow fields, ORCA, flocking and Reduced-LOD integration), C3a (root motion for agents) and C4 (flight) built.

Source: manifold-nav (crates/manifold-nav/) (wasm-safe logic crate: api always, the engine behind the default engine feature), the integrator inputs in manifold_player::integrator::character (crates/manifold-player/src/integrator/character.rs), the server wiring in manifold_server_state::nav (crates/manifold-server-state/src/nav/) and the world change log in manifold_world::change_log (crates/manifold-world/src/change_log.rs). Tests: crates/manifold-nav/tests/ (stored voxel fixtures under tests/fixtures/; flight in flight.rs; doors, climbing, hazards, swimmers, walker_large and surface groups in terrain.rs; flow fields in flow.rs), the steering unit tests in manifold-nav’s steer.rs, crates/manifold-player/tests/character_bit_identity.rs, manifold-player’s integrator::climbing_tests, crates/manifold-server-state/tests/motion_override_steps.rs and the nav::tests module of manifold-server-state.

One integrator, three inputs

integrate_character_tick(state, input, motion, knock, frame_yaw, tick, sampler, params, dt) is the one movement step for players and agents. state is CharacterMotionState (position, velocity, grounded, mode, ground frame and carry; Daedalus D §4.1):

InputTypePlayersAgents
Body constantsCharacterParamsCharacterParams::PLAYER, the old global constantsthe profile’s box, step, jump velocity and walking speed
DriveCharacterInputPlayer(&MovementInput): keys, horizontal velocity from yawAgent(&DriveInput): analog world velocity, vertical, jump
OverrideOption<&MotionOverride>from MotionOverrides by the step’s client input tickfrom MotionOverrides by the server tick

With the player’s body and keys, no override, no knock and no carry it is today’s integrator bit for bit (a verbatim copy of the pre-C0 integrator is compared over seeded scenes; the canary hash is pinned).

Knock and carry (Daedalus D §6.6) are the integrator’s, for players and agents alike. knock is a one-shot world Δv: its vertical part goes into velocity (an upward knock clears on_ground), its horizontal part into carry_velocity; a flyer carries the whole knock. Carry adds to the input’s or drive’s velocity (not an override’s), decays 12 s⁻¹ grounded or flying and 0.5 s⁻¹ in the air, is capped at 80 m/s, and a contact no step-up took stops it on that axis. The step also reports a landing or wall ImpactSample (Daedalus D §10.2). An agent’s carry lives in its CharacterCarry component, server-side; navigation keeps only the knockback lock.

Overrides. A MotionOverride is one integrated step’s displacement, keyed by that step’s tick label: velocity (world frame for Ability(id), character frame for RootMotion: −Z forward, rotated by the facing with manifold-det), yaw_delta (agents turn their facing by it past the turn-rate cap; players discard it), vertical: Some(v) (gravity suspended), start_tick, ticks, stop and source. At most one applies to a step; RootMotion beats Ability. While one applies the keys or drive and jump are ignored and look still applies. Every applied step returns an OverrideReport { requested, achieved, stopped } (stopped: an Ability override with COLLISION hit something against its direction; movement ends it). Reports collect per entity in MotionOverrideReports.

Writers push into the entity’s MotionOverrides component; the server player loop and AgentMovementSystem look each step up and prune expired entries; the client predictor looks up the local player’s and RingEntry records the applied override, so replay after a correction reproduces it.

Root motion for agents (C3a, §5.7). Pygmalion’s AnimLogicSystem pushes an agent’s RootMotion override for the server tick while an Animation-authority play leads its root (see animated characters (docs/architecture/animated-characters.md)); AgentMovementSystem applies it that tick. Its yaw_delta turns the facing past the turn-rate cap, and the step reports MotionSource::Scripted(ScriptedMotion::RootMotion { play, start_tick }): the leading playback’s source (a machine key or 0x8000 | group) and its start tick, from RootDelta::lead. B’s overrides report ScriptedMotion::Ability { id, start_tick }. A play integrates every tick at any LOD: the override wakes an agent at rest, and a Reduced agent never strides over a tick with an override, so each step’s displacement lands with its own dt. F presents a root-motion play by snapshots (committed root-motion segments are F2’s): the publisher leaves segment mode on the play’s first step with a handover snapshot. An authored charge is such a play; code-driven Dash and Leap stay for simple motion.

A player entity without a live connection collides against the server’s own loaded chunks (FrontierSampler over WorldSpace::is_chunk_loaded).

Climbing. A body whose climb_speed is positive and which overlaps a climbable cell (CollisionSampler::climbable) climbs instead of falling: DriveInput::climb Up/Down move at ±climb_speed, Hold stays, and None slides at −0.15 · climb_speed, or climbs when the body presses into a wall. Players keep climb_speed 0, so they never reach the branch (the bit-identity tests pin it). CharacterIntegration::climbing reports it.

Agents

An agent is an entity with manifold-nav:locomotion (Tracked<Locomotion>, server-only, persisted). Archetypes name its profile:

components: {
    "manifold-sim:position": (), "manifold-sim:velocity": (),
    "manifold-sim:yaw": (), "manifold-sim:pitch": (), "manifold-sim:motion-mode": (),
    "manifold-nav:locomotion": (profile: "flagship-game:boar"),
},

Adding it adds MoveIntent, NavAgent, LocomotionState, MovementLod, InWorldSpace and AgentMotion (F’s replication marker). Agents never carry ControlledBy and never read PendingInputs.

ComponentWritten byRead by
MoveIntentD’s tasks (desired_velocity, jump, sprint, look_at, follow: Path(handle), speed_percent)the follower, the controller
NavAgentthe follower (goal, corridor, pending splice or replan, progress watch)F (Corridor::lookahead, Corridor::epoch)
LocomotionStateAgentMovementSystem every integrated tick (actual velocity, edge, MotionSource, cruise speed, stop distance, landed, knockback carry)F’s segments, D’s hearing; never animation
MovementLodD’s LOD pass (Full, or Reduced { k } with k = 2 or 4)the follower (request priority), steering (ORCA eligibility), AgentMovementSystem (the stride)
Locomotion.headthe controllerAgentMovementSystem, which copies the achieved head yaw into D’s AiPresentation.look_yaw (F’s agent view reads it)

The body itself is the usual Position, Velocity, Yaw (the facing), Pitch (head pitch), MotionMode and OnGround, written at the server tick.

Tick order

Time → HeldActionsApply → ResolveActionHandles → (D's AI block)
  → NavMaintenance → NavFollow → Steering
  → AnimBind → AnimLogic → PlayerMovement → AgentMovement
  → PhysicsInteraction → PhysicsStep → AnimHistory → …

The player movement system reads nothing navigation writes, and agents move after it, so players are byte-identical whether or not agents exist (tested).

Profiles

data/locomotion/*.ron entries of the manifold-nav:locomotion registry (server side of the data stage), keyed by id: in the mod’s namespace: class (manifold:walker_small, manifold:walker, manifold:walker_large, manifold:swimmer, manifold:flyer), body, speeds, step, jump apex, max_drop, gap_jump: Auto | Blocks(n), turn_rate_deg_per_s, arrive_radius, strafe, can_open_doors, can_climb, per-path-type malus (an integer or Impassable), drop_malus, surface_malus (block names and a cost, below), search, avoidance and knockback. A body larger than its class envelope fails the stage; a profile that restricts its class is a warning, because its Yes answers become searches. The flyer class requires can_fly: true, and can_fly: true requires the flyer class; a flyer cruises at speed.walk (sprint when sprinting). The swimmer class requires can_swim; can_climb requires a climbing class (walker_small, walker) and a positive speed.climb, which becomes the body’s climb_speed. Compilation (manifold_nav::compile) derives the integer cost table, the jump velocity sqrt(2 g jump), the gap reach floor(walk · 2v/g − 0.5) (0 in a class without gap jumps) and the move rules. Door and climbable cells a profile cannot use cost Impassable without making it restrict its class: reachability gates them itself (terrain).

LayerWhatWhere
0One byte per cell per class: path type and floor in eighths, in 4³ bricks; classified against the integrator’s collision boxes, fluid::query::sample_body and the values’ traits (terrain); a surface-group plane where some cell has a groupclassify, brick, blocks
1Local components per chunk (symmetric moves; door cells are single-cell nodes), external and one-way edges per cell, and labels over the open graph and, when doors exist, the sealed graph: open islands, strong components, closures (≤ 512 components per island), island signatures, islands with climbable cells, and a negative cachegraph
2Bounded, resumable A* on snapshots: integer costs, (f, h, offset key) tie-break, slices of 256 expansions, priority caps, partial pathssearch
CorridorString-pulled waypoints after the start cell, which it keeps as origin (the take-off of an action edge the path begins with; segments ≤ 8 blocks), per-tick revalidation of the next four segments, splices of ≤ 256 nodes to the next valid waypoints, replans on the slow timercorridor, agent
FlightThe flyer class’s masks, the swept straight-line test, the greedy brick search and flight corridors (below)flight

The server’s ServerNav resource holds NavWorld and the corridor index (brick → agents). NavMaintenanceSystem reads each space’s world change log (WorldSpace::changes_since), reclassifies the dependency box of every edited cell, recomputes Layer 1 for changed chunks and relabels, builds chunks within three chunks of every awake agent, and pumps requests. Worker builds classify from manifold_world::ChunkSnapshots (shared with physics’s terrain collider builds) and are installed only if the chunk’s ChunkStamp (load serial and generation) still matches the snapshot’s.

For D (brains).

  • NavWorld::reachable(profile, space, from, &goal) -> Reach: Yes, No or Unknown with a reason and the space’s nav epoch. No is never wrong for the class; store the epoch with a give-up and re-ask once nav_epoch(space) has advanced.
  • nav::request(world, NavRequest) -> PathHandle (the server helper over NavWorld::request and request_toward): never blocks; a No fails the handle at once without a search. Then put MoveIntent::follow_path(handle) on the agent.
  • status(handle), result(handle), release(handle). The follower installs the result, repairs and replans by itself; if repair proves the goal unreachable the handle’s status becomes Failed(Unreachable(..)).
  • NavGoal::Within { target: Entity(..), range } goals are chased: the search goes to the entity’s position at request time and the follower follows it as it moves (straight at it within 4 blocks, a replan when it leaves the corridor’s end by 2 blocks).
  • line_walkable, is_standable, nearest_standable, estimate_travel, find_spawn_cells(&SpawnQuery, max, &dyn LineOfSight, out).
  • NavEvents (a resource the follower refills each tick, read by the next AI block): NavEvent::DoorAhead { cell } when an agent stops before a closed door (doors). A task opens it with use-block; profiles with can_open_doors do so by default.

For F (presentation). LocomotionState::{source, edge, cruise_speed, distance_to_stop}, NavAgent.corridor with lookahead(from, points, len) and epoch (bumped by installs, splices and teleport). While a segment presents the agent, distance_to_stop is the segment’s: the path left to its Stop at the tick (PathSegment::distance_to_stop_at_tick, the integer evaluation clients share), inside arrive_radius, else 0; otherwise the follower’s remaining distance. No binding source reads it yet.

For everyone. nav::teleport(world, agent, pos, yaw) places an agent’s body instead of writing Position (and zeroes its carry). Knockback is Daedalus D’s character::CharacterImpulses: AgentMovementSystem takes an agent’s entry at its step (a pending knock wakes an agent at rest), the integrator applies it, and the controller starts a knockback lock during which the stuck watch is suspended. Agents’ landings and wall hits join the CharacterImpacts stream.

Crowds

Phase C3 (§10, §13, §5.5).

Shared flow fields (flow (crates/manifold-nav/src/flow.rs)). A field is keyed by (space, profile, goal): the goal’s entity for a Within { Entity } goal, else its anchor cell. Two ways in:

  • Promotion. NavWorld::request (and request_toward) counts requests toward each goal; once 8 agents of one profile hold requests toward one goal (NavSettings::flow_promote_agents, 0 turns it off), a new request returns a path handle backed by the goal’s field (NavWorld::flow_of(handle)): status is Searching until the field’s first build lands, then Ready with no waypoints, and D’s tasks follow it unchanged. Flyers and Away goals are never promoted.
  • Explicit (E’s waves): nav::request_flow(world, FlowRequest { space, profile, goal, cover }) returns a FlowFieldHandle (an entity goal is resolved and followed); members follow MoveIntent { follow: Some(Follow::Flow(handle)), .. }; nav::release_flow when done. NavWorld::flow_status(h) is Building, Ready { version }, Failed(..) or Released.

A build (FlowJob (crates/manifold-nav/src/flow.rs), on the nav workers, in slices of 4,096 cells) covers the region around its users, its goal and E’s cover points grown by 16 blocks (at most 160 across from the goal): every standable cell, its Layer 2 moves reversed, an integer Dijkstra from the goal cells popping by (cost, cell index) (so a cell’s cost equals the cheapest Layer 2 path’s inside the region, on every host), a line-of-sight flag for cells on the goal’s level with a straight walkable line to it within 16 blocks, and each cell’s successor and edge kind, stored per chunk. A field rebuilds (at most 4 Hz, the old one in use until the new lands) when a Layer 0 change lands in its region, when a follower stands outside it, or when its goal moves more than 2 blocks from the anchor; the server moves entity goals each tick in NavMaintenanceSystem. A field nobody holds is dropped 200 ticks later.

Following a field. The follower traces 24 cells along the field from the agent’s cell (a line-of-sight cell goes straight to the goal cell), string-pulls them into the agent’s corridor and follows that like a path’s, so F’s segments, the corridor index, doors and the stuck watch work unchanged. It re-traces when the trace runs short (on its last waypoint, unless that is the goal), the field is rebuilt, the trace is cut, or stuck recovery would splice or replan; field followers never search. A re-trace bumps the corridor epoch only when the waypoints ahead changed: F reissues the agent’s segment on a new epoch, so re-tracing the same route sends nothing. An agent outside the region heads straight for the goal when the line is clear (and holds otherwise) until the next build covers it.

Steering (steer (crates/manifold-nav/src/steer.rs)). SteeringSystem separates every agent; a profile’s avoidance adds more:

avoidanceWhat
Separation (default)Reynolds separation only
OrcaRVO2’s ORCA (1 s horizon, ≤ 10 neighbours within 4 blocks) while the agent is at Full LOD, not knocked, within 16 blocks of a player and has ≥ 4 bodies within 3; separation otherwise. Other ORCA agents share the avoidance; players, knocked agents and agents that only separate are obstacles it avoids alone
Flockseparation, alignment and cohesion with agents of the same profile within flock.radius, weighted by the profile’s flock: (separation, alignment, cohesion, radius) (defaults 1, 0.3, 0.2, 4 m); 3D for flyers

Reduced LOD (§5.5). D’s LOD pass writes MovementLod. A Reduced { k } agent follows every tick, only separates, and caps its own requests at Low; it integrates every k-th tick (staggered by its durable id) with dt the ticks since its last step, but only while F’s publisher presents it through a path segment (AgentMotionState::mode == Segment) and the step is plain walking: grounded, no override (so an Animation-authority play integrates every tick), no knock or carry, no action edge or jump ahead, not swimming or climbing, and facing within one tick’s turn of its heading. Otherwise it integrates every tick. LocomotionState::step_tick is the tick the body’s state stands for; the publisher judges a held agent’s segment only on its step ticks, and clients never see the held pose (they evaluate the segment).

Flight

A flyer is a profile of class manifold:flyer (envelope 0.98 × 1.95). Its Layer 0 byte is Open (the envelope, feet on the cell’s bottom and centred, touches no collision box and no fluid a body cannot swim), Walkable (also directly above a collision top: a hover-clearance cell), HazardFluid or Blocked. Per 4³ brick, NavChunk.flyer (FlyerMasks (crates/manifold-nav/src/flight.rs)) says blocked, open (every cell free, none a hover cell), or mixed with a free and a hover u64 (bit x + 4y + 16z within the brick). Edits refresh the bricks whose bytes changed. There is no Layer 1 for flyers.

  • Straight line (flight::segment_clear, NavWorld::line_flyable, and line_walkable for a flyer profile): the body’s box swept from feet a to feet b. An Amanatides–Woo walk over bricks skips stretches whose swept box lies in all-free bricks and walks cells only where it does not; every foot cell under the swept box must be free. Conservative: it never passes a line that touches geometry.
  • Reachability: Yes (StraightLine) when the straight line from the start’s cell to a free goal cell is clear, otherwise Unknown (FlightSearch). Never No; no negative cache.
  • Search (a flyer’s request; SearchJob::with_flight): the straight line first, which gives a one-waypoint corridor without a search. Otherwise a greedy search whose nodes are open bricks and, in other bricks, free cells, six neighbours each, one unit per node plus one per hover cell, f = g + 4·h (Manhattan, in cells), with the ground search’s bounds, caps, arena and tie-break. Its node path is emitted as axis-aligned runs through free cells and string-pulled with the swept test (segments ≤ 16 blocks). Every waypoint’s edge is Fly, its feet on its cell’s bottom.
  • Following: arrival as on the ground, the next four waypoints’ cells each tick, every remaining segment on a corridor-index hit (the index samples a flight segment over the body’s two foot rows, from its start) and once at install when planned against an older epoch; the look-ahead steers at the furthest waypoint within four blocks that a clear segment reaches; chasing flies straight at a target within four blocks.
  • Movement: a flyer integrates in MotionMode::Fly (the agent step sets it); the controller clamps the 3D desired velocity and drives its vertical part through DriveInput::vertical, never jumps, and when horizontally blocked slides vertically toward the target’s side. A hovering flyer can rest. A knock goes wholly into carry; its lock ends by ticks alone.

Terrain

Phase C2: doors, climbing, hazards and slow cells, walker_large, swimmers and surface groups.

What values are. Classification keys on Enki A’s traits and family roles, which do not exist yet. manifold_nav::blocks::NavBlockTraits states them in Enki A’s vocabulary over today’s block names, and NavBlocks is the per-value table built from them for the block registry (ServerNav::set_block_traits, or NavWorld::set_block_traits; a change rebuilds the space’s navigation):

Enki AThe seam
manifold:climbablemember(CLIMBABLE, block)
manifold:nav_slowmember(NAV_SLOW, block)
manifold:nav_hazard (fire, other)value(NAV_HAZARD, block, "fire")
family roles door, fence_gate, trapdoor and the property openopenable(role, closed_block, open_block) (today two block types)

When Enki A lands, NavBlocks::build reads its trait bitsets, family roles and open instead, and nothing downstream changes. The flagship declares none of these yet, so its worlds classify as before.

Hazards and slow cells. A hazard value in a cell the body occupies (or stands on) makes the cell DamageFire/DamageOther, impassable unless a profile prices it; within one cell of the body’s cells a walkable cell is DangerFire/DangerOther (base cost 8). A slow value in the body’s cells or under it makes a walkable cell Slow (base cost 8). Fire outranks other.

Doors

  • Layer 0. A cell is a door cell when a door value lies in the body’s own cells: DoorClosed when the envelope fits once every closed door there is swapped for its open value, DoorOpen when it is open already. Doors never support a body.
  • Moves. Door cells are entered only by a level or step move along an axis, as a Door edge (no diagonals, jumps, drops or gap jumps into or diagonally out of them). Everyone may stand in DoorOpen; DoorClosed only profiles with can_open_doors (base cost 8).
  • Layer 1. Door cells are single-cell components, so a door never merges the rooms on either side, and opening or closing one changes only Layer 0. Profiles that open doors answer from the open graph; others from the sealed graph (door nodes removed), where a goal reachable only through a door is Unknown (DoorGated) and the search decides.
  • Following. When the next corridor edge enters a closed door cell, the follower stops within 1 + w/2 of it, raises DoorAhead once, and holds (the stuck watch rebased) until Layer 0 shows the cell open. After 40 ticks the cell joins NavAgent::avoid for 600 ticks (cost 2,048 per entry) and the agent replans; every request made for the agent through nav::request carries its avoided cells.
  • use-block (Talos G op 27) is one Enki B transaction (block transactions). nav::block_use_transaction(spaces, blocks, &BlockUseRequest { space, pos, expect, actor, owner }) builds it: requester Native("talos"), one blocks section that expects the seen value over every cell the use changes and fills them with the door’s value with open flipped (the cell and the run of the same value straight above and below it, so a two-high door’s halves flip together or not at all). NavFollow submits it with TxnEngine::submit through its declared TxnEngine write; exclusive code uses transaction::submit. Outcomes come from transaction::take_results(world, &Requester::Native("talos")): Committed, Rejected or Conflict(Changed) when the door changed first. The use applies at B’s slot after the tick, and maintenance sees the door open the next tick. The default door behaviour acts as Rule("ai.brain").

Climbing

A climbable cell is one whose body overlaps a climbable value while unsupported (Climbable); a supported cell in front of a ladder stays walkable, so profiles that cannot climb keep it. Climbing classes (walker_small, walker) get Climb edges up into and down out of climbable cells, and a rise out of one onto a ledge is a Climb, not a jump. The controller climbs toward a Climb edge’s waypoint (onto a ledge: up until the feet clear its lip), and the server’s sampler answers climbable from the value table (blocks::TraitSampler). A profile that cannot climb is restricted only in islands with climbable cells: there its Yes becomes Unknown (ProfileRestricted). Climbable values should not collide: a ladder panel inside the cell blocks the class envelope.

walker_large and swimmers

  • walker_large (1.98 × 2.95, a 2 × 2 footprint, step and jump one block, drops of four, no gap jumps). Its foot cell is the footprint’s low corner, so its body centre is (0.5, 0, 0.5) from the foot cell’s centre (LocomotionClass::footprint_offset): NavWorld queries and the follower shift positions into foot-cell space, while corridor waypoints (and Corridor::lookahead) are foot-cell feet.
  • Swimmers stand only in Water cells, where the whole envelope (feet on the cell’s bottom) lies in a swimmable fluid, and move level and vertically between them. The controller rises or dives toward the depth of the waypoint it steers to.

Surface groups

surface_malus masks name blocks (Enki A’s masks later), checked against the world’s blocks at load (data stage). NavBlocks sorts values by the set of masks they match into surface groups (u8, at most 255) and keeps each profile’s cost per group; a chunk stores a surface plane only when some cell has a group (a standing cell takes its support’s group, a water cell its fluid’s). Search adds the entered cell’s surface cost; a negative one lowers the heuristic’s step cost, and every step still costs at least 4.

Workers and determinism

Builds and searches run on the global rayon pool (at most workers in flight: 2 native, 1 in the browser’s server worker) and synchronously where there are no threads. The executor is manifold_world::exec, shared with physics’s terrain collider builds. A worker build classifies a snapshot of the chunk and its 26 neighbours; an edit meanwhile reclassifies nothing in a chunk not yet installed. So a build of a chunk edited since the snapshot is classified again from the current blocks, and one whose neighbour was edited, loaded or unloaded since is installed and then has the band facing that neighbour reclassified like an edit. Results apply in request order per space. NavSettings::lockstep() completes every job at dispatch and delivers it two ticks later, for tests and determinism checks. Costs, tie-breaks and island logic are integers, facing is a 16-bit binary angle with manifold-det trigonometry, and hash maps are lookup-only; tests/determinism_guard.rs scans the sources.

Limits

  • No digging (C5). Traits, door roles and surface masks come from NavBlockTraits until Enki A; masks name blocks only.
  • Flow fields are one integration per region (no per-portal tile sharing between fields, no incremental repair; an edit rebuilds the field), are keyed by profile, and do not serve flyers (Talos C §28, issues 46–48).
  • Nothing drains Talos’s transaction results yet (navigation watches the door); the ring keeps at most 256 until Talos G’s results step.
  • String pulling ignores surface costs, so a corridor segment can cut a corner across a costlier surface the search went round.
  • Flyers have no component graph: an unreachable flight goal costs a capped search per request (Talos C §28, issue 32).
  • Layer 1 relabels the whole class graph when anything changed, and an edit recomputes its chunk’s components; an edit costs about 1 ms rather than the 40 µs budget.
  • At-rest wake-ups from bodies are not built. Reduced-LOD agents integrate coarsely only while a segment presents them (issue 50).