Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
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 governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
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 governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Guides13 min read

Gameplay API

On this page
On this pageEngine components, such as lightsScoped actor accessAuthored values and runtime stateRuntime component membershipEntry pointsErrors: compile time versus runtimeExecution and initializationTyped cross-actor fieldsGenerated declarations and editor RebuildSwarm populationsDiagnostics and validationLive inspection Back to top

defineActor defines an actor type by composing components. Spawning creates an instance. defineComponent declares ordinary Zig data, optional lifecycle hooks, and a generated scene/editor schema. Content discovery registers exported actors and their components automatically. There are no legacy defineEntity or Entry aliases.

For storage tradeoffs and examples, start with Choosing component storage: attachable, embedded, and swarm.

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

pub const Spin = hi.defineComponent(.{
    .name = "spin",
    .data = struct {
        degrees_per_second: f32 = 45,
        state: struct { elapsed: f32 = 0 } = .{},

        pub fn update(self: *@This(), actor: hi.ActorContext, tick: *const hi.TickContext) void {
            self.state.elapsed += tick.dt;
            var rotation = actor.rotationEuler();
            rotation[1] += self.degrees_per_second * tick.dt;
            actor.setRotationEuler(rotation);
        }
    },
});

pub const Beacon = hi.defineActor(.{
    .archetype = "beacon",
    .components = .{ hi.ComponentRender, Spin },
});

Components are attachable by default. Their fields have defaults, appear in the inspector when supported, and serialize under the component's stable name. Use .storage = .embedded for fixed composition when needed: data lives in the actor payload, with the same scene and inspector schema. Storage does not change the callback API. Actor component tuple order defines execution order within a tile; .requires = .{OtherComponent} validates presence, not scheduling order.

Engine components, such as lights

Built-in components join the same actor composition tuple. They declare engine capabilities; their runtime data is accessed through subsystem APIs.

zig
pub const LampLogic = hi.defineComponent(.{
    .name = "lamp_logic",
    .requires = .{hi.ComponentLight},
    .data = struct {
        intensity: f32 = 10,

        pub fn start(self: *@This(), actor: hi.ActorContext) void {
            actor.render().setLight(actor.id, .{
                .kind = .point,
                .color = .{ 1, 0.8, 0.6 },
                .intensity = self.intensity,
                .radius = 8,
                .cast_shadow = true,
            });
        }

        pub fn onChanged(self: *@This(), actor: hi.ActorContext, _: []const u8) void {
            actor.render().setLight(actor.id, .{ .intensity = self.intensity });
        }
    },
});

pub const Lamp = hi.defineActor(.{
    .archetype = "lamp",
    .components = .{ hi.ComponentLight, LampLogic },
});

requires rejects an actor composition missing the light capability. The light is configured in start; onChanged keeps it synchronized with inspector edits and target.component(LampLogic).set(.intensity, 20). ComponentLight is a capability marker, not a public light-data struct, so target.component(hi.ComponentLight).set(.intensity, 20) is not available. Use actor.render().setLight(actor.id, patch) or hi.render().setLight(target, patch) to update the engine light directly. For a scene-authored light without custom behavior, the tuple can contain only hi.ComponentLight. Other built-ins, such as ComponentRender, ComponentCamera, and ComponentPhysics, compose the same way.

Scoped actor access

Lifecycle hooks belong to component Data, and actor events fan out in component tuple order. See Actor and component lifecycle for every event's dispatch source, activation order, teardown guarantees, and the current limits of runtime-added components.

All ordinary lifecycle hooks receive hi.ActorContext as their second argument:

HookArguments after selfReturn
awakeactor, ?*const hi.SceneActorViewvoid or error union of void
start, onDestroyactorvoid
onEnable, onDisableactorvoid
update, lateUpdate, fixedUpdateactor, *const hi.TickContextvoid
onCollisionactor, *const hi.CollisionInfovoid
onAnimationEventactor, *const hi.AnimationEventvoid
onMessageactor, *const hi.Messagevoid
onChangedactor, []const u8void
onEnvironmentPreviewactor, hi.EnvironmentPreviewvoid

deinit(self, allocator) releases owned data at storage teardown or replacement by a custom scene decoder; it does not receive an actor context. Initialization and scene codecs retain their distinct construction contracts.

Initial activation follows awake → start → onEnable. Later effective activity changes dispatch onDisable / onEnable without rerunning start. These hooks follow the actor's authored active state combined with layer presentation; they do not add independent enabled flags to ordinary components.

actor.id is the generation-safe weak handle. actor.get(Health) returns an optional *Health.Data; actor.require(Health) requires presence. These resolve embedded, column, and instance-added data.

Important

Contexts and returned pointers are borrowed for the callback only. Retain ActorRef across ticks, not pointers. Do not hold sibling pointers across structural world mutations.

position, rotationEuler, scale, and their setters resolve transforms only when called. No transform lookup is charged to callbacks that do not use one. actor.world(), actor.render(), and the event helpers expose world services. Builtins remain capability markers; use these subsystem services to operate them.

Parallel tick hooks use hi.ParEntry, with direct transform data and deferred commands. They cannot use ordinary actor lifecycle hooks. Parallel access remains explicit through .parallel = true and does not grant sibling/world access.

Authored values and runtime state

Ordinary fields are authored. Put temporary values in an inline state field:

zig
speed: f32 = 5,
state: struct {
    elapsed: f32 = 0,
    grounded: bool = false,
} = .{},

The whole state field is excluded from inspector schemas, scene output, and replication. Applying authored settings preserves existing state, and scene input that tries to author it is rejected. Defaults initialize it on spawn. This convention adds no allocation, indirection, or generated instance wrapper. Owned state still needs deinit; the generated scene decoder can skip that state and decode ordinary authored values. Custom scene codecs remain responsible for their own ownership policy.

metadata.properties remains available for labels, ranges, widgets, and individual transient fields when a flat script-facing path matters. metadata.runtime_only hides the inspector surface; it does not mean all values are transient. Use state for that distinction.

Runtime component membership

Entry points

CallerAddRemoveResult
Typed actor viewtarget.component(C).add()target.component(C).remove()Zig !void
Typed componentC.addTo(target)C.removeFrom(target)Zig !void; same implementation
Name-based Zig / toolshi.world().componentAdd(target, "health")hi.world().componentRemove(target, "health")Zig !void
Parallel component callbackentry.cmd.addComponent(C, target)entry.cmd.removeComponent(C, target)void; records a worker command
KawaActors.add(actor, "health")Actors.remove(actor, "health")Boolean request acceptance

Check committed presence with target.component(C).has(), C.hasOn(target), hi.world().componentHas(target, "health"), or Kawa Actors.has(actor, "health"). There is no implicit add-on-write, replacement operation, multiple instances of the same type, or automatic actor migration. Add/remove changes membership, not the actor's archetype or identity. Export runtime-only components so content discovery registers them even when no actor tuple includes them.

zig
try target.component(Health).add();       // Health.Data defaults
try target.component(Health).remove();
try Health.addTo(target);                 // Equivalent component-first form
try Health.removeFrom(target);
try target.component(hi.ComponentLight).add();
try target.component(hi.ComponentLight).remove();

Register the component with the game/module before attaching it. One actor has at most one instance of each component type. add reports AlreadyPresent; remove reports NotPresent. Missing requirements return DependencyMissing; removing a requirement still used by another component returns RequiredByOther. Requirements are checked in queued command order; the engine does not implicitly add dependencies or cascade removals.

Errors: compile time versus runtime

Typed operations on .embedded produce a compile error, including worker commands. catch cannot intercept a compile error. Passing a defineSwarm to ActorRef.component also fails to compile: a swarm has no actor component view. For names only known at runtime, use the name-based World API and catch its error:

zig
hi.world().componentRemove(target, "door_controller") catch |err| switch (err) {
    error.NotAttachableStorage => return, // Embedded membership is fixed.
    error.NotPresent => return,           // Already absent.
    else => return err,
};

The enclosing function in this example returns !void.

Runtime errorMeaning
EntityNotInWorldTarget is dead, retiring, stale, or not in this World.
UnknownComponentThe name is not registered in this World.
NotAttachableStorageThe registered type is embedded or otherwise not attachable.
MandatoryComponentAttempt to structurally change Transform, which every actor requires.
AlreadyPresentAdd requested for a component already present in projected membership.
NotPresentRemove requested for a component absent from projected membership.
DependencyMissingA required component would be missing at this point in the command sequence.
RequiredByOtherA remaining component requires the component being removed.
OutOfMemoryStorage or command allocation failed.
Initialization/script errorsA synchronous attachment failed during initialization; the error propagates.

These APIs return an open Zig error union; keep an else branch when handling a subset. The table describes public validation and common failure paths, not an exhaustive list of every user-defined awake error.

Execution and initialization

Outside gameplay callbacks these calls commit synchronously. Within a dispatcher callback they enqueue a generation-checked command. Commands commit at the next World tick or tick-group/fixed-phase structural boundary, after borrowed rows and worker tiles are no longer in use. try confirms validation and queue acceptance; a later allocation or initialization failure is logged. Commands raised by transition hooks wait for a subsequent boundary. has, field reads, and field writes see committed membership: adding and immediately setting a field inside the same callback does not work. Put initial user values in Data defaults or awake; configure newly attached system components in a subsequent callback.

Parallel callbacks record the same changes with entry.cmd.addComponent(Health, target) or entry.cmd.removeComponent(Health, target). These commands drain in deterministic slice/append order, then commit at a structural boundary. Parallel code must use this command buffer; calling ActorRef/HostApi mutation methods from worker threads is unsupported.

Worker command methods have no catchable return value: recording/drain failures are diagnosed by the command system. Kawa returns false for a rejected request; it does not expose Zig error values. None of the deferred forms returns a future, completion receipt, or transaction. A failed queued command is logged and later commands still run with fresh validation. Queued commands targeting a retired actor are discarded, never redirected to a recycled actor slot. A sequence of several operations is not atomic; earlier successful operations stay committed.

Queue required components before dependents, and queue removal of dependents before requirements. Removing then adding the same type queues a fresh instance. A retained typed view resolves that replacement; it does not identify a particular attachment generation. Do not retain raw row pointers across callbacks.

An added component receives awake(null), then start and onEnable when the actor's startup/activity permits. Removal sends final onDisable when enabled, then onDestroy, then releases scripts, owned data, and system resources. Re-adding constructs fresh defaults and a fresh lifecycle. This also works for attachable components declared in defineActor: their fixed payload slot becomes absent and can later hold a new column row. Weak component views remain valid as views, but resolve whichever instance is currently attached.

StorageRuntime membershipMutable contents
User .attachableAdd/remove, whether declared by the actor or added laterYes
System componentsAdd/remove with subsystem construction and cleanupThrough their system APIs
TransformMandatory; cannot removeYes
.embeddedFixed within the actor payload; cannot add/removeYes, including runtime state
defineSwarmFixed population schema; rows are not actors and accept no component attachmentsYes; use hi.swarm(C).spawn, retire, get, set, and column operations

An actor with embedded components can still gain and lose other attachable components. Immutability describes membership/layout, not the actor's values or the swarm population size. Runtime changes affect the current World; they do not rewrite the authored scene document. Inspector authoring remains a document edit.

Optional system types are Render, Physics, Light, Camera, Audio, VisualZone, Decal, FogVolume, Animation, Particle, and Kawa. Use each system's configuration API after attachment; attaching a row alone does not provide a render asset, audio clip, particle program, animation graph, or script asset. Resource creation and publication still follow that subsystem's normal asynchronous work. Physics removal/re-addition uses attachment generations to reject old simulation replies.

See Choosing component storage for layout costs and why embedded and swarm membership stays fixed. The Runtime Components example demonstrates declared and extra attachments, system configuration after commit, lifecycle counters, fresh defaults, typed call directions, field paths, and catching an unsupported name-based request.

See Component lifecycle for dispatch and batching details. Kawa uses Actors.add(actor, "health") and Actors.remove(actor, "health"), returning whether the request was accepted.

Typed cross-actor fields

zig
const hp: ?u64 = target.component(Health).get(.hp);
_ = target.component(Health).set(.hp, 100);
_ = target.component(Motion).set(.velocity, .{ 1, 0, 0 });
_ = target.component(Motion).setAt(.velocity, 1, 2);
const elapsed: ?f32 = target.component(Health).getPath("state.elapsed");
_ = target.component(Health).setPath("state.elapsed", 2.5);

target.component(Health) creates a typed view holding only the weak ActorRef; it does not resolve or retain a data pointer. Each operation resolves the current component afresh, so the view can be retained across ticks. has() checks presence; creation itself does not imply the actor or component exists. Swarms cannot be accessed through actors.

get and set accept a compile-time Component.Field enum generated from the top-level fields of Component.Data, so .hp has a concrete enum type for contextual completion and misspellings fail to compile. getPath and setPath accept compile-time strings for nested exposed fields or computed paths such as "state." ++ "elapsed". The view methods inline into the same field accessors, with no allocation or runtime selector or path lookup. The example's Health.Data.state contains elapsed: f32 = 0.

Selectors and paths must identify an exposed, safely copyable field; selecting a whole nested struct still requires direct ActorContext.get access. get returns the declared Zig type wrapped in an optional for missing/dead actors or components; optional fields therefore have an outer presence optional. set accepts exactly the declared type and reports success. Arrays/vectors additionally support getAt(.field, index) / setAt(.field, index, value), and nested arrays/vectors use getPathAt("nested.field", index) / setPathAt("nested.field", index, value). Indexing checks bounds and copies the field value. Strings and AssetRef use the same get/set names; setters intern borrowed strings before publication. Owning pointers are not exposed through typed copies.

Both call directions are supported and share the same implementation:

zig
_ = Health.field(.hp).set(target, 100);
_ = target.component(Health).set(.hp, 100);

const elapsed = Health.fieldPath("state.elapsed").get(target);
const same_elapsed = target.component(Health).getPath("state.elapsed");

The component-level factories also expose accessor types for generic code, including their Value and Element types. Prefer the actor-bound view for ordinary gameplay; component-first access remains a supported public API. Direct assignment through actor.require(Health).hp remains callback-scoped pointer mutation and does not call onChanged automatically.

Field paths resolve at compile time; typed calls use a component ID and field index, without runtime string searches. Values cross the host boundary as exact typed byte copies, validated against the registered field type and size. Wide integers never pass through f64. Writes notify onChanged and retain numeric script synchronization. Direct sibling pointer mutation is ordinary gameplay data access and does not imply a change notification. Dynamic tooling and Kawa keep hi.actors/Actors.* name-keyed APIs.

Generated declarations and editor Rebuild

Component.Field, typed accessors, lifecycle adapters, and scene/editor schemas are Zig compile-time declarations. They do not produce a separate source file. Their implementation starts in src/hikari/sdk/src/define_component.zig and src/hikari/sdk/src/define_component/; actor composition lives in src/hikari/sdk/src/define_actor.zig. Actor-bound component views live in src/hikari/sdk/src/component_ref.zig. The compiled declarations become part of the game module.

Content discovery does write <game-source>/content_manifest.gen.zig, beside root.zig. sdk/build/content_manifest.zig rescans the game's Zig sources and rewrites this import list on every build-script invocation, including editor Recompile (the toolbar action whose tooltip is "Rebuild the game module"). Added, renamed, and removed source files are therefore reflected on the next build. Discovery reads the filesystem and does not use Git tracking, so ignored projects such as Stylized and Bistro work the same way. Do not edit generated manifests by hand.

The editor invokes zig build game-module from the project's resolved SDK root. Zig tracks imported source changes and reevaluates the affected compile-time declarations. Under the default project layout, editor build outputs live at:

OutputLocation relative to the project
Local/global Zig caches.engine/cache/game/zig/local/ and global/
Plugin composition sources.engine/cache/game/zig/local/hikari-plugins/<build-key>/
Built game library on macOS.engine/lib/libgame.dylib
Staged reload generation on macOS.engine/hot-reload/gen-<n>/game.dylib
Full compiler log.engine/logs/game-module-build.log

On success, the editor loads a fresh generation, rebuilds scene registrations, and refreshes the inspector. Replacement waits until the editor can safely swap modules; compilation failures leave the existing module running.

Recompile uses the selected SDK: an explicit engine_sdk path, otherwise a bundled sdk/ beside the editor executable, with a monorepo fallback to src/hikari/sdk/. It does not refresh a bundled SDK from engine source or rebuild the running editor host. Kaji's editor packaging restages bundled SDK inputs when their content hash changes. Engine/host ABI changes require rebuilding and restarting the editor; ordinary game component changes use Recompile.

Swarm populations

zig
pub const Crowd = hi.defineSwarm(.{
    .name = "crowd",
    .capacity = 100_000,
    .data = struct {
        x: f32 = 0,
        y: f32 = 0,
        z: f32 = 0,

        pub fn integrate(store: *hi.SwarmStore(@This()), dt: f32) void {
            for (store.liveIndices()) |index| store.column("x")[index] += dt;
        }
    },
});

Use hi.swarm(Crowd) for store operations. A swarm has no actor identity, hierarchy, actor scripts, or actor lifecycle hooks. Supply bulk integrate or per-row update(self, SwarmEntry, dt), not both. Capacity is required and nonzero. The existing World-owned SoA storage and deferred retirement path are retained.

Diagnostics and validation

Unknown declaration options, malformed dependency tuples, unsupported hook signatures, and invalid typed field paths fail at the declaration. New options must be explicitly admitted by the SDK; misspellings never silently disable behavior.

From src/hikari, run zig build test-sdk compile-fail -Dgame-src=<absolute game> for authoring contracts. zig build test covers host lifecycle and field access; zig build entity-bench measures dispatch. Build actual products with Kaji.

Live inspection

Play's read-only inspector reflects live transform, system and public user fields, including components attached after spawn. Added sections carry a Runtime pill. Normal callback assignments and field/system setters participate automatically; hidden, transient, and Data.state remain excluded. Only the selected actor is observed, with coalesced notifications rather than per-frame field polling. See live actor inspection for the callback invalidation policy and the prepared Play-only edit boundary.

PreviousDevelopment guideNext Choosing component storage

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/gameplay-api.md
On this pageEngine components, such as lightsScoped actor accessAuthored values and runtime stateRuntime component membershipEntry pointsErrors: compile time versus runtimeExecution and initializationTyped cross-actor fieldsGenerated declarations and editor RebuildSwarm populationsDiagnostics and validationLive inspection Back to top