System guide · Headless TypeScript NPCs
TypeScript game AI with navigation that stays outside the render loop.
Treat NPC intelligence as four cooperating layers: decisions choose an intention, pathfinding plans a route, locomotion advances a headless agent, and React Three Fiber presents the resulting position and heading.
Do not make one update function responsible for every kind of AI.
Decision
A behavior tree or schedule chooses patrol, follow, go-to, wander, or idle.
Global navigation
A* finds a valid route around static walls. HPA* reduces search work on large grids.
Local locomotion
The agent consumes the route at a world-units-per-second speed and avoids moving obstacles.
Presentation
An R3F component copies position and heading into a group. It does not own the rules.
This split lets the same NPC run in a browser, a deterministic test, or a Node.js simulation. It also makes failures diagnosable: a bad decision is different from an unreachable destination, a blocked crowd, or a model with the wrong forward axis.
Build one navigation grid, then ask pure functions for routes.
Overworld's grid A* uses eight-direction movement, prevents diagonal corner-cutting, inflates circular obstacles by the agent radius, and smooths the result when line of sight permits.
import {
createNavGrid,
findPath,
} from '@overworld-engine/ai'
const grid = createNavGrid({
bounds: { minX: 0, maxX: 80, minZ: 0, maxZ: 80 },
cellSize: 1,
agentRadius: 0.45,
obstacles: level.colliders.map(({ x, z, radius }) => ({
x, z, radius,
})),
})
const route = findPath(grid, [4, 6], [62, 51])
if (route === null) {
showUnreachableFeedback()
}Tile maps can use createNavGridFromCells so one wall value blocks exactly one cell. When doors or obstacles change, update the grid deliberately. A route is a snapshot of navigability, not a promise that the world will remain still.
Let a headless agent own movement state, not a mesh.
import { createAgent } from '@overworld-engine/ai'
const guard = createAgent({
grid,
position: [4, 6],
speed: 1.8,
random: seededRandom,
avoid: {
obstacles: () => crowdColliders,
lookahead: 1.5,
agentRadius: 0.4,
stuckAfterMs: 1200,
},
})
guard.patrol(
[[4, 6], [20, 6], [20, 18]],
{ pauseMs: 800 },
)
// Browser, server, or test loop:
const status = guard.update(deltaMs)The agent reports plain position, heading, movement state, behavior, and arrival events. Inject a random source for reproducible wandering. Pass elapsed time intoupdate so movement stays frame-rate independent and can be replayed.
Dynamic avoidance is a deterministic local steering layer. It deflects the current step around moving circles without rewriting the planned route. If every direction remains blocked, the agent can replan after a configured timeout.
Compose intentions with a behavior tree and a shared blackboard.
import {
createBehaviorTree,
sequence,
selector,
condition,
goToAction,
patrolAction,
} from '@overworld-engine/ai'
const tree = createBehaviorTree(
selector(
sequence(
condition(({ blackboard }) => blackboard.alert),
goToAction(guard, alarmPosition),
),
patrolAction(guard, patrolPoints, { pauseMs: 800 }),
),
{ alert: false },
)Sequences and selectors remember a running child, while conditions can re-evaluate changing world facts. Keep perception and combat policy in your game layer; write those facts into the blackboard or expose them through conditions. The framework supplies composition and agent actions, not a universal enemy brain.
For ambient NPCs, schedules can map phases such as day, dusk, and night to declarative behaviors. That is often clearer than forcing every shopkeeper into a combat-style decision tree.
Use one driver and make the scene adapter visually thin.
import { NPCWalker } from '@overworld-engine/ai'
function Guard() {
return (
<NPCWalker
agent={guard}
tree={tree}
rotationOffset={Math.PI}
>
<GuardModel />
</NPCWalker>
)
}Passing the tree makes NPCWalker tick the decision tree and agent once per frame. Do not also call agent.update elsewhere or the NPC will double-step. If your simulation owns the clock, use driven={false}; the component will only mirror the latest position and heading.
Scale route planning separately from NPC count.
For large grid worlds, createHierarchicalGrid builds an HPA*-style cluster graph. findPathHierarchical searches that smaller graph, refines each segment in bounded windows, and falls back to full-grid A* when needed. Rebuild the hierarchy after changing the underlying grid.
- Test blocked targets, narrow corners, unreachable regions, and dynamic doors.
- Inject time and randomness; assert plain agent status without mounting React.
- Measure visited nodes before adopting hierarchical navigation.
- Keep animation state derived from locomotion instead of feeding it back into AI.
This package is grid navigation, behavior composition, schedules, and local avoidance. It is not a navmesh generator, crowd simulator, perception model, animation graph, or machine-learning runtime.
Put AI inside a complete renderer-independent RPG architecture
Connect headless NPC state to quests, dialogue, networking, and an R3F scene.
Connect moving NPCs to proximity and interaction →Read the React Three Fiber RPG architecture guide →