AI Hunters
App: apps/ai_hunters/
The AI Foundation full loop (engine PLM-189 / ADR 0065). Four hunters run a complete enemy-AI loop authored as a behaviour tree in Wren — the engine ticks each tree at AI cadence, with no per-frame plumbing:
perceive (sight: range + field-of-view + masked-raycast line-of-sight; and sound: the noise bus) → chase the target around a central obstacle via the navmesh, taking a distinct surround-ring slot and picking a tactic by range → when the target escapes and goes silent, lose it → search its last-known point → give up and return to spawn.
It wires the whole scriptable-AI surface: BehaviourTree /
Bt / Blackboard, Ai.nearestVisibleTarget,
LostTargetTracker, NoiseBus /
AlertMemory, Tactic, Nav, and
SurroundRing.
Run from root
Section titled “Run from root”% ./plume3d ai_huntersThe demo self-screenshots the encircle, search, and give-up frames, then exits.
The tree
Section titled “The tree”Each hunter builds its tree once, in Hunter.new. The game never ticks it — the engine drives
every registered tree each frame (Ai.tickInterval = 0 here, for smooth motion). Leaves are
closures over self; they return a BtStatus.
_tree = BehaviourTree.new()_tree.setRoot(Bt.selector([ Bt.sequence([ Bt.leaf { |bb, dt| perceive_(bb, dt) }, // updates the FSM; success only while Engaged Bt.leaf { |bb, dt| chase_(bb, dt) } // navmesh-chase to the ring slot + pick a tactic ]), Bt.sequence([ Bt.leaf { |bb, dt| searching_(bb, dt) }, // success only while Searching Bt.leaf { |bb, dt| search_(bb, dt) } // head for the last-known point ]), Bt.leaf { |bb, dt| giveUp_(bb, dt) } // return to spawn]))Perceive — sight AND sound
Section titled “Perceive — sight AND sound”Ai.nearestVisibleTarget does range + field-of-view + line-of-sight + nearest in one native call;
its LOS is a masked raycast against the obstacle’s static physics body (so a target behind the block
is unseen). Noise comes from the bus. The result feeds the lost-target FSM, and the last-known point
tracks the last real perception:
perceive_(bb, dt) { var seen = Ai.nearestVisibleTarget(_scene, _x, 0.5, _z, _facingX, 0, _facingZ, 35.0, 220.0, [0, _target[0], 0.5, _target[2]], _mask) >= 0 var heard = _alert.active() if (seen) { bb.setNumber("lkx", _target[0]); bb.setNumber("lkz", _target[2]) } else if (heard) { var a = _alert.anchor(); bb.setNumber("lkx", a[0]); bb.setNumber("lkz", a[2]) } var st = _fsm.tick(dt, seen || heard) // LostTargetTracker → TargetState bb.setInt("state", st) return st == TargetState.engaged ? BtStatus.success : BtStatus.failure}The obstacle is a static physics body on an obstruction layer, so the LOS raycast has something to hit:
Physics.addStaticBox(_scene, _obstNode, 3, 1.5, 3)Physics.setCollisionLayer(_obstNode, _mask)The world sends footstep noise through the bus each few ticks; every hunter that hears it raises its
AlertMemory:
var heard = _noise.emit(_target[0], 0.5, _target[2], 6.0) // -> ids that heard itfor (id in heard) _hunters[id].hear(_target[0], _target[2])Chase, surround, tactic
Section titled “Chase, surround, tactic”The engaged branch claims a distinct SurroundRing slot, navmesh-paths to
it (routing around the obstacle), and records a tactic for the current range:
var slot = _ring.assign(_id, 1.0, 1.0, true, _id * 90.0) // [angleDeg, radius, standby]// ... convert (angleDeg, radius) to a world point around the target, then Nav.findPath to it ...bb.setInt("tactic", Tactic.select([2, Tactic.melee, 6, Tactic.strafe, 30, Tactic.kite], dist, Tactic.approach))Lose → search → give up
Section titled “Lose → search → give up”When the target flees far away and goes silent, sight and sound both drop; the
LostTargetTracker moves engaged → searching (the hunters head for the last-known point) and,
after the search window, → gaveUp (they return to spawn). The hunters are coloured by FSM state —
their own colour while engaged, amber while searching, dim blue once they give up — so the three
screenshots read as the loop’s three phases. Placement is a seeded, navmesh-snapped scatter, so the
run is deterministic.
See also
Section titled “See also”- AI API — the whole scriptable-AI surface.
- Physics: spatial queries & collision layers —
overlapSphereandRaycast.fromPointMasked, the perception primitives this composes with.