A defineSwarm population can run the same Brain definitions as actors. Each
member owns execution state; one population owner supplies the immutable program,
sensing, target positions, scheduling and movement. Brain assets and code-authored
Brains use identical task and transition semantics.
Swarm rows are not actors. They have no actor messages, attached components or
individual actor inspector. Use a World-owned component or subsystem to bridge
those services, then operate on the population's columns in bulk. The Example's
src/ai_crowd.zig demonstrates this with an embedded crowd_ai owner and a separate
ai_crowd_member swarm. No actor is created for each crowd member.
Program and execution storage
hi.ai.population.Program.bind(definition) validates and copies a definition
into population-owned value storage. When loading an asset, call it once with the
callback-scoped result of world.brainProgram(asset). Keep the copy, never the
borrowed pointer. The owner must belong to one World; destroying it releases that
population's state. A new Play loads the newly cooked asset.
hi.ai.brain.CompactInstance uses the same executor as Instance, retaining the
machine and latest decision record, without an override array or history ring.
On the tested macOS arm64 build it occupies 48 bytes, versus 560 bytes for
Instance. Position, targets, observations and routes are additional storage.
const hi = @import("hikari_game");
pub const Crowd = hi.defineSwarm(.{
.name = "crowd",
.capacity = 1000,
.data = struct {
x: f32 = 0,
y: f32 = 0,
z: f32 = 0,
execution: hi.ai.brain.CompactInstance = .{},
search_seconds: ?f32 = null,
},
});The owner starts each new row with execution.start(definition) before spawning
it. During simulation, call decide(definition, facts) and
stepWithParameters(definition, position, targets, dt, overrides). The override
slice is indexed by the definition's parameter slots: null or an absent slot
inherits the shared default. Resolve named slots when binding the definition.
This lets populations keep overrides in separate fields instead of reserving all
16 parameters in every execution instance.
Runtime(parameter_capacity, history_capacity) provides intermediate storage
choices. A local override can only address a slot below parameter_capacity.
Instance remains Runtime(16, 16); CompactInstance is Runtime(0, 0).
All definitions keep the existing Brain capacities and supported task set.
Scheduling, sensing and events
population.Decisionsperforms round-robin admission overliveIndices()and a due-time column.begin(dt, limit)advances simulation time and resets the work budget.next(live, due)admits at most that limit, scanning at most the live population once. The caller sets each admitted member's next due time. Zero, negative or non-finite dt admits no work.- Keep the previous service time per member and pass accumulated simulation time to task execution. If a condition enters a new state now, do not charge that new state's timer for time before its entry. The sample starts its next task step at the following admission. Movement follows cached routes every frame.
population.Subjectdistinguishes actor identities from swarm identities. Swarm identity includes population ID, slot and generation. It is World-local; do not move these identities or service state between Worlds.scheduler.Scheduler(population.Subject).Service(...)supplies the existing spatial candidate discovery, bounded observations, fairness and query budgets for mixed actor/swarm identities.scheduler.Service(...)remains the actor specialization. The game supplies snapshots and publishes results into columns.Vision.querySegmentallows a swarm observer with no actor collider to cast an occlusion segment. Pass an invalid actor as the exclusion. Subject identities do not create physics bodies; adapters still choose how to resolve any target collider. The sample uses swarm observers and ordinary actor targets.population.Stimuli(capacity)receives stimuli once through the owner'sonMessage. Each admitted member checks the bounded inbox using distance and category filtering. Its sequence cursor prevents duplicate delivery. The newest audible event wins; old events expire, overflow replaces the oldest, andoverwrittenexposes inbox pressure. Start new members at the inbox's current sequence so recycled slots do not inherit earlier noises.
Budgets bound admitted work, not CPU milliseconds. Snapshot gathering, sorting, memory maintenance, rendering and motor integration still cost work proportional to population size. Admission is currently synchronous; it does not dispatch jobs. The sample reserves separate budgets for its actor and swarm sensing services.
Playable test
Open Tutorials → AI Swarm Population (scenes/ai_population.json) and start Play.
The dedicated scene starts with 100 members. Choose 1000 to scale up or
0 to retire the population. It contains no stationary load guards or door encounter. Creation
and removal are limited to 32 members per simulation tick.
All members share assets/brains/crowd.brain.json:
- Blue members patrol; hearing noise turns them amber while investigating.
- Purple members search for the shared
search_secondsduration. - Detecting a target takes priority over noise and turns members red as they flee.
- Members have 6 m or 11 m hearing and different speeds. All initially inherit the Brain's search duration; the data model also supports individual overrides.
Use Noise to place a stimulus or Move player to move a visible target. Use Inspect, then click near a member to select it. The panel displays its slot/generation, state, transition reason, hearing, speed, selected target and bounded decision history. The yellow outline marks the selection. History starts with the latest record at selection and records subsequent decisions; selecting another generation clears it. Retire selected / respawn replaces that member with freshly initialized data while maintaining the requested population size.
The owner actor also exposes those diagnostics through the ordinary live actor inspector. This is an explicit population inspection adapter; there is no general editor hierarchy or inspector for arbitrary swarm rows yet.
The panel reports state counts, decisions, rays, route plans, CPU time, decision lateness, observation age, inbox overwrites and reserved data bytes. Reserved data counts the owner's runtime and member field payloads at capacity, excluding swarm bookkeeping, GPU resources and the actor's general overhead.
Change the Brain's search-duration default, let it cook, then start a new Play to apply it to the whole population. Pause freezes decisions and movement. Scene unload or owner disable retires members and clears the visual binding.
The fixture admits at most 128 decisions, 128 perception steps, 32 sight rays and 8 route plans per tick. It uses the existing flat-ground planner and shared scene obstacle snapshot. Members route around static walls; member-to-member avoidance, crowd packing, navmeshes and animation are not implemented by this sample. Rendered members are small instanced markers.
Headless verification
From src/hikari:
zig build test -Dtest-filter=population -Dtest-filter=brain -Dtest-filter=scheduler -Dtest-filter=vision -Dgame-src=<absolute example/src>
zig build ai-population-bench -Dgame-src=<absolute example/src> -Doptimize=fastSee the measured benchmark results.
The benchmark runs 100 and 1,000 moving members sharing a code-authored Brain, with parameter overrides, a noise event, actor-target perception and selected member tracing. It checks admission limits and delivery to every member, and reports mean/p95 CPU time, worst decision lateness, noise delivery delay, observation age and storage sizes. Analytic visibility replaces physics; these numbers exclude the playable adapter, navigation, rendering and GPU work. The game-package test separately drives 1,000 real swarm rows through the sample's Brain/movement loop and checks route budgets and retirement/recycling.
See Brain assets, component storage, and Game AI for the underlying contracts.
Custom gameplay tasks
Populations can implement gameplay tasks with a borrowed member adapter and Registry.Driver(0). Share the bound table, issue an owner for each member lifetime using Subject.swarm, and cancel before retirement. Capability masks reject actor-only actions during binding. The existing moving crowd fixture keeps its movement/wait behavior and compact execution storage.