Skip to content
hikari
RenderingEditorAIToolchainDocumentation
Download

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsDownloadLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Guides11 min read

Brain assets

On this page
On this pageEdit a BrainDecisions and tasksBrain document formatRun an asset from ZigAuthor a Brain entirely in ZigParameters and the guard exampleInspect a running guardSDK and ownershipSwarm populations Back to top

A Brain is a reusable AI state/task program. Author a .brain.json document, assign it through a component's Brain picker, and let Shinra compile it to .shinbrain. Actors share the immutable compiled program; each actor keeps its own current state, timers, parameter overrides, and decision history.

The AI playground provides a complete example. Both guards reference asset://./brains/guard, while their hearing ranges remain 6 m and 11 m.

Edit a Brain

In the Asset Browser, choose New Brain, or open an existing Brain asset. The editor uses the same graph toolbar, canvas, stage, and inspector controls as the material and animation graph editors.

  • Canvas: state cards show the task, duration or target slot, and an entry badge. A single directed connection groups every condition and task result between the same two states; its badge shows the number of routes. Hover or select a connection to see its label. Click a grouped connection to choose an individual transition, Complete, or Failure result in the inspector. Global rules originate at Any state. The Connections selector filters these layers. Self completion/failure destinations stay in the inspector instead of adding redundant loops; explicit self-transition rules draw a loop above the card.
  • Focus selection: enabled by default. Selecting a state shows its incoming/outgoing connections and global rules; selecting a connection isolates that connection. Click empty canvas space to restore the overview, or turn off Focus selection to keep the complete graph visible. This changes only presentation.
  • State inspector: click a card to edit its name and task, select completion/failure destinations, and set movement or wait options. Set as entry changes the starting state. Its transition list includes outgoing and global rules.
  • Transition inspector: click a single connection or choose a transition from a grouped connection to select its source and destination, toggle restart, and choose Any value, Must be true, or Must be false for each fact. Earlier/Later changes rule priority; the inspector’s numbered transition rows reflect this order.
  • Brain settings: use the gear button to select the entry state, edit target-slot capacity, and add or edit shared duration parameters and named facts.

Use State on the toolbar or double-click empty canvas space to add a state. Drag from an output pin to another state's input pin to create a transition, or use Transition in the inspector. Pan, zoom, fit, and reset use the shared graph controls. Drag cards to arrange them; their optional editor_position coordinates are saved with the source and excluded from the cooked runtime tables. The draggable Any state card uses the root editor_any_state_position field.

Names use ASCII letters, digits, and underscores, with a maximum of 47 bytes. Renaming a state, parameter, or fact updates its references. Removing a state also removes its condition transitions and redirects task/entry references to a remaining state; the final state cannot be removed. Removing a duration parameter converts its uses to literal durations. A fact used by a condition must be detached before deletion. Remove deletes the selected inspector subject. Delete/Backspace over the canvas deletes the selected state or condition arrow; on an individually selected task result it resets that destination to the source state. Selecting a grouped connection never bulk-deletes its routes; choose an individual member first.

Valid edits save automatically after a short idle delay; the toolbar save button writes immediately. Invalid names, missing destinations, contradictory conditions, invalid numbers, and exceeded capacities prevent saving. Undo/Redo uses the editor's normal document history, including card movement. The canonical JSON Schema is src/hikari/schemas/com.hikari.brain.v1.schema.json, staged into the project's .engine/schemas/ directory.

Live validation sends the latest document to Shinra. If the asset pipeline is offline, edits remain available for saving and the toolbar reports offline status; further edits do not lose that status. Reconnection submits the latest document for validation.

The ordinary Shinra watcher cooks saved changes. Wait for cooking to finish before starting Play. Programs already loaded by a World remain fixed until that World is destroyed; a new Play loads the new program. Invalid or unavailable programs do not start guard movement, and brain_status reports the binding problem.

Decisions and tasks

The game publishes a set of named boolean facts and supplies target positions. The Brain evaluates transitions in authored order. The first matching transition wins; all required facts must be true and all excluded facts false. An empty source matches any state. A transition to the active state is skipped unless Restart is enabled. Each decision call can enter at most one state.

custom runs a registered gameplay task, with typed parameters and explicit cancellation. Its implementation is available from both Zig and assets. move_to returns a ground destination for the game's motor/navigation adapter. wait consumes simulation time. Completion and failure destinations belong to the state. One task step performs at most one completion transition. Failure reports carry the activation serial, so an old movement result cannot complete a replacement task. The game invokes decisions and task steps at its simulation boundary; pausing the playground stops both.

Targets, hearing categories, movement, target selection, and the meaning of facts remain game-owned. The engine executes the shared tables. A Brain does not directly call physics, render, or control an actor transform.

Brain document format

This small noise probe enters search when it hears a stimulus, waits for a parameterized duration, then returns to idle. Save it as assets/brains/noise_probe.brain.json. It demonstrates the format independently of the seven-state guard adapter.

json
{
  "$schema": "../../.engine/schemas/com.hikari.brain.v1.schema.json",
  "kind": "com.hikari.brain",
  "version": 1,
  "entry": "idle",
  "target_count": 0,
  "facts": ["noise"],
  "parameters": [{ "name": "search_seconds", "value": 4 }],
  "states": [
    { "name": "idle", "task": "wait", "duration": 3600,
      "next": "idle", "failure": "idle" },
    { "name": "search", "task": "wait", "duration_parameter": "search_seconds",
      "next": "idle", "failure": "idle" }
  ],
  "transitions": [
    { "name": "heard_noise", "to": "search", "restart": true,
      "require": ["noise"] }
  ]
}

entry selects the initial state. next and failure name each state's completion and failure destinations. Transition from may be omitted or null to match any state; require and exclude reference the declared boolean facts. Here restart makes another noise restart the search timer. Idle is also a timed task and completes back to itself after an hour; there is no indefinite-wait task in this format.

target_count declares the positional slots supplied by game code. A move_to task selects one with target_slot and finishes inside arrival_radius, unless finish_on_arrival is false. The task returns a destination; the game supplies routing and locomotion. duration_parameter selects a named default, with an optional per-instance override. Current Brain parameters are nonnegative duration values; patrol points, speed and hearing settings belong to the consuming component.

Shinra validates names, references and capacities, then cooks the source to immutable numeric tables in .shinbrain. Product asset loading reads that binary, without parsing JSON. See HBR2 layout for the wire format.

Run an asset from Zig

This attachable component consumes the noise-probe asset above. It filters ordinary gameplay stimuli, supplies the noise fact, and advances the shared program using simulation time. It has no movement task or physics requirement.

zig
const hi = @import("hikari_game");

pub const NoiseProbe = hi.defineComponent(.{
    .name = "noise_probe",
    .data = struct {
        brain: hi.AssetRef = hi.AssetRef.must(.brain, "asset://./brains/noise_probe"),
        hearing_range: f32 = 8,
        search_seconds: f32 = -1,
        state: struct {
            execution: hi.ai.brain.Instance = .{},
            ready: bool = false,
            pending_noise: bool = false,
            noise_bit: u5 = 0,
            retry_in: f32 = 0,
        } = .{},

        pub const metadata = .{ .properties = .{
            .brain = hi.UserDataPropertyOptions{ .widget = .{ .resource = .brain } },
        } };

        pub fn onEnable(self: *@This(), _: hi.ActorContext) void {
            self.state.ready = false;
            self.state.pending_noise = false;
            self.state.retry_in = 0;
        }

        pub fn onDisable(self: *@This(), _: hi.ActorContext) void {
            self.state.execution.cancel();
            self.state.ready = false;
            self.state.pending_noise = false;
        }

        pub fn onChanged(self: *@This(), actor: hi.ActorContext, _: []const u8) void {
            self.onDisable(actor);
            self.onEnable(actor);
        }

        pub fn onMessage(self: *@This(), actor: hi.ActorContext, message: *const hi.Message) void {
            if (!self.state.ready) return;
            const noise = hi.ai.Stimulus.fromMessage(message) orelse return;
            if (noise.heardBy(actor.position(), self.hearing_range, 3) == .heard)
                self.state.pending_noise = true;
        }

        pub fn update(self: *@This(), actor: hi.ActorContext, tick: *const hi.TickContext) void {
            if (!(tick.dt > 0)) return;
            if (!self.state.ready) {
                self.state.retry_in -= tick.dt;
                if (self.state.retry_in > 0) return;
                self.state.retry_in = 0.25;
            }
            const def = actor.world().brainProgram(self.brain) orelse return;
            if (!self.state.ready) {
                if (def.target_count != 0) return;
                self.state.noise_bit = def.fact("noise") orelse return;
                self.state.execution.overrides = @splat(null);
                const duration: ?f32 = if (self.search_seconds == -1) null else self.search_seconds;
                if (!self.state.execution.setParameter(def, "search_seconds", duration)) return;
                self.state.execution.start(def);
                self.state.ready = self.state.execution.machine.enabled;
                if (!self.state.ready) return;
            }
            const facts: u32 = if (self.state.pending_noise)
                @as(u32, 1) << self.state.noise_bit
            else
                0;
            self.state.pending_noise = false;
            _ = self.state.execution.decide(def, facts);
            _ = self.state.execution.step(def, actor.position(), &.{}, tick.dt);
        }
    },
});

Export the component through your game's normal gameplay registration, attach it to an actor, and emit a stimulus as shown in Game AI. The mask 3 accepts categories 0 and 1. This probe coalesces noises into one fact per update and ignores messages until its program is ready. It retries unavailable assets or invalid bindings at most four times per simulation second. A production adapter should expose the binding failure, as the guard does through brain_status.

Each actor owns its Instance; the World owns the shared definition. search_seconds = -1 inherits the asset default, and a nonnegative value overrides it. The definition pointer is local to the callback. Keep the AssetRef, reacquire the definition on each callback that needs it, and resolve named fact/parameter bindings again when changing the asset. Do not reset the instance every frame: that would restart its task timer and history.

For a movement adapter, consume step(...).destination, retain the current execution.machine.activation with the route request, and pass that captured serial to execution.fail(def, activation) if the request fails. Cancel obsolete movement when a decision changes state. Inspect execution.recent(0) for the latest reason and def.state_names[execution.machine.current] for the active state while the instance is enabled. The complete guard adapter also supplies visual facts, target slots, a motor and read-only inspector diagnostics.

Author a Brain entirely in Zig

Game code can build the same immutable tables at compile time. This is useful for code-owned behavior and host-free tests. Changes require recompiling the game; this definition has no Asset Browser document or picker entry. It still supports per-instance overrides, named conditions and the same bounded trace.

zig
const std = @import("hikari_std");
const hi = @import("hikari_game");

const search_brain = blk: {
    @setEvalBranchQuota(10_000);
    break :blk hi.ai.brain.compile(.{
        .entry = "idle",
        .target_count = 0,
        .facts = &.{"noise"},
        .parameters = &.{.{ .name = "search_seconds", .value = 4 }},
        .states = &.{
            .{ .name = "idle", .duration = 3600, .next = "idle", .failure = "idle" },
            .{ .name = "search", .duration_parameter = "search_seconds", .next = "idle", .failure = "idle" },
        },
        .transitions = &.{.{ .name = "heard_noise", .to = "search", .restart = true, .require = &.{"noise"} }},
    }) catch @compileError("Invalid search Brain");
};

test "two actors share code-owned behavior with independent timers" {
    var first: hi.ai.brain.Instance = .{};
    var second: hi.ai.brain.Instance = .{};
    first.start(&search_brain);
    second.start(&search_brain);
    try std.testing.expect(first.setParameter(&search_brain, "search_seconds", 1));
    const noise = @as(u32, 1) << search_brain.fact("noise").?;
    _ = first.decide(&search_brain, noise);
    _ = second.decide(&search_brain, noise);
    _ = first.step(&search_brain, .{ 0, 0, 0 }, &.{}, 1);
    _ = second.step(&search_brain, .{ 0, 0, 0 }, &.{}, 1);
    try std.testing.expectEqual(search_brain.state("idle").?, first.machine.current);
    try std.testing.expectEqual(search_brain.state("search").?, second.machine.current);
}

For small index-based programs that do not need named conditions, parameters or history, hi.ai.Program + hi.ai.Machine remain available. Machine.start, signal, step, fail and cancel execute directly over borrowed hi.ai.State slices. Brain instances wrap this same task executor and add the authored decision tables and trace.

Parameters and the guard example

The guard supplies visible, remembered, retry_ready, and noise facts. Target slot 0 is its next patrol point, slot 1 its last heard position, and slot 2 its visual pursuit/remembered position. The sample adapter binds the named states patrol, investigate, look_around, return_to_patrol, pursue, last_seen, and search.

Patrol offsets, speed, hearing range, and vision settings remain ordinary per-actor properties. The Brain's search_seconds parameter defaults to 4. The guard property of the same name defaults to −1, which inherits the Brain default; a nonnegative actor value overrides it. The two authored guards both inherit the shared default.

To check reuse:

  1. Open brains/guard.brain.json, select search_seconds, change Default seconds, and Save.
  2. Start a new Play in AI Perception & Brains. Both guards use the edited duration.
  3. Make noise near a guard, then move a detectable target into its cone. Check the transition reasons as investigation gives way to pursuit.
  4. Break sight and watch last-seen movement, search, and return to patrol. Give one guard an explicit search_seconds override in Edit to compare independent instances.

Inspect a running guard

Select a guard during Play. Its read-only component fields expose:

  • brain_status and active_state_name;
  • selected target handle, visibility, last known position, and observation age;
  • transition_name, decision_reason, and activation count;
  • decision_history: recent transitions, newest first, with simulation time, source/destination names, and reasons.

The SDK keeps the last 16 transition records, including the fact mask and activation serial. The guard formats them into a bounded inspector text buffer; long histories may show fewer full lines. The in-game panel also shows the latest transition name and Brain binding status. Task completion, movement failure, explicit cancellation, and authored conditions all produce history entries. This is a decision trace, not World rewind.

SDK and ownership

hi.ai.brain.Document describes authoring data. compile validates code-owned definitions and resolves names to table indices. The editor asks Shinra to validate authored documents asynchronously. Asset-backed runtime programs read HBR2 through decode; code-owned definitions may be compiled into the game as shown above.

world.brainProgram(asset) returns a callback-scoped borrow of a World-owned program, or null when unavailable/invalid. Keep the AssetRef across callbacks and reacquire the borrow when needed. Never store the returned pointer in actor state. The World caches at most 32 distinct programs, loading each once and releasing its owned storage at destruction. Play and Edit caches are independent.

hi.ai.brain.Instance owns execution state. Use start, setParameter, decide, step, complete, fail, cancel, and recent. Parameter overrides are indexed against the bound definition; clear them when rebinding to a different program. No instance retains a program, World, or actor pointer. Warm decisions and task steps allocate nothing.

Each program supports up to 64 states, 128 transitions, 32 boolean facts, 16 duration parameters, and 32 target slots. The existing perception scheduler budgets sensing; Brain decisions and navigation still run at the game's chosen cadence. Hierarchy, utility scoring, and custom task catalogs are not supported by this asset format.

Swarm populations

The same Brain executor also runs in swarm rows. See Brain execution for swarm populations for compact state, shared asset binding, mixed actor/swarm sensing, bounded decisions, and the moving 1,000-member acceptance scene.

PreviousGame AI: perception and movement playgroundNext Brain execution for swarm populations

Documentation follows the current engine checkout.

Snapshot 86f12d68Source docs/guides/brain-assets.md
On this pageEdit a BrainDecisions and tasksBrain document formatRun an asset from ZigAuthor a Brain entirely in ZigParameters and the guard exampleInspect a running guardSDK and ownershipSwarm populations Back to top