Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
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
Systems10 min read

Actor communication (hi.actors)

On this page
On this pageShort path (preferred)Choose the right channelConceptsFind and livenessZigKawaMessagesFrame orderZig send / receiveKawa send / receiveDelivery fan-outComponent field accessZig — prefer target-first component accessSame-actor typed Zig (ActorContext)API quick referenceZig hi.actorsZig ActorContext message helpersZig defineComponent field helpersKawa Actors.*Related (not the unified namespace)ComparisonDemo sceneImplementation mapPerformance notesSee also Back to top

Hands-on walkthrough: Tutorials — First messages.

Game modules never hold engine Entity pointers. The public surface for find, liveness, messages, and name-keyed component fields is:

LanguageNamespace
Zighi.actors (alias hi.Actors) — sdk/src/actors.zig
KawaActors.* — distinct from self-only Actor.* (transform, scene id, …)

hi.world().send / World.find / Entity.is_alive still work as aliases for older samples; new code should use hi.actors / Actors.*.

Short path (preferred)

From an ActorContext lifecycle callback — sender is implicit (e.id):

zig
pub fn update(self: *@This(), e: hi.ActorContext, ctx: *const hi.TickContext) void {
    _ = .{ self, ctx };
    e.sendEventTo("msg_door_zig", "open", .{});
    e.emitEvent("ping", .{});
    e.sendEvent(target, "damage", .{ .args = &.{ .{ .float = 25 } } });
}

With only a self ActorRef (e.g. update(self, id, ctx)):

zig
const a = hi.actors;
a.sendEventToFrom(id, "msg_door_zig", "open", .{});
a.emitEventFrom(id, "ping", .{});
a.sendEventFrom(id, door, "damage", .{ .args = &.{ .{ .float = 25 } } });

Optional find + component fields:

zig
if (a.tryFind("enemy_00")) |enemy| {
    if (enemy.component(Health).has()) {
        const hp = enemy.component(Health).get(.hp) orelse 0;
        _ = enemy.component(Health).set(.hp, hp - 10);
    }
}
kawa
Actors.send("msg_door_zig", "open");
Actors.emit("ping");
if (Actors.has(enemy, "health")) {
    let hp = Actors.get_number(enemy, "health", "hp");
    Actors.set_number(enemy, "health", "hp", hp - 10);
}

Fire-and-forget does not need a prior isAlive check (dead targets no-op). Use tryFind / isAlive when the branch depends on presence. Prefer e.sendEvent* so you never type .from = id.


Choose the right channel

GoalUseAvoid
A tells B “open / damage / ping” (any language mix)Message send / emitString method invoke, globals
A reads/writes B’s numeric component fieldstarget.component(Comp).get(.field) / set(.field, value) in Zig; getNumber / setNumber for dynamic namesStuffing large state into message payloads
A reads/writes string or asset fieldsThe same typed get / set in Zig; getString / setString / getAsset / setAsset for dynamic namesFree-form invent outside component schema
Query / tag actors by catalog gameplay tagshasTag / addTag / removeTag / withTagFree-form tag invent — catalog only (configs/gameplay_tags.json)
Same actor, Zig component A → typed sibling BActorContext.get / require / has in lifecycleName-keyed path on hot same-actor loops when you have types
Cache a neighbour across framesStore ActorRef, check isAlive before useRaw pointers; assuming the ref keeps them alive
Prevent unloadExisting loose layer / session scope onlyHolding a ref (refs never pin)

Cross-actor method call by name is intentionally not provided. Messages are the function-call substitute at actor boundaries.


Concepts

PieceRole
ActorRefGeneration-safe handle (EntityId alias). Weak: never retains. Dead → soft no-op. See Refs.
Scene idStable authored string ("msg_door_zig"). Preferred address for find / sendToSceneId.
Message nameInterned verb ("open", "ping").
PayloadUp to four MessageValues (bool / int / float / vec3 / entity ref / interned name).
sendPoint-to-point to one actor.
emitBroadcast to every registered message listener.
ListenersOn delivery: Zig components with onMessage (declaration order), then all Kawa script slots with on_message. Inactive / terminating actors skip.

Find and liveness

Zig

zig
const hi = @import("hikari_game");
const a = hi.actors;

const door = a.find("msg_door_zig");       // scene id → ActorRef (or .invalid)
_ = a.findByName("DoorZig");               // display name, O(n) — prefer scene id
if (a.tryFind("msg_door_zig")) |d| {       // live only
    _ = d;
}
if (!a.isAlive(door)) return;              // generation still live
if (!a.isActive(door)) return;             // live and entity-active

Kawa

kawa
let door = Actors.find("msg_door_zig");    // handle or nil
let by_name = Actors.find_by_name("DoorZig");
if (Actors.is_alive(door) == false) return;
if (Actors.is_active(door) == false) return;
// Self: Actors.find(Actor.get_scene_id()) when you need a handle for Actors.*

Rules

  • Always treat stored handles as maybe dead after despawn, scene replace, or layer unload.
  • isAlive / dead send / field access: no crash — false / null / no-op.
  • Refs do not stop unload. Lifetime extension is only loose actors (layer_key null) or session services, not game handles.

Messages

Language-agnostic bus: Zig ↔ Zig, Zig ↔ Kawa, Kawa ↔ Zig, Kawa ↔ Kawa. One payload ABI and one World-owned event dispatcher; deferred delivery by default. Both language APIs resolve addresses and enqueue into this same dispatcher.

Frame order

text
collision events    → may enqueue
Zig entity updates  → may enqueue
Kawa entity updates → may enqueue
flushMessages()     → deliver (multi-pass, budgeted)
purge terminated

Enqueue is O(1). Delivery is deferred so handlers do not re-enter mid-update. Nested enqueues run breadth-first in subsequent passes, with at most 8 passes and 4,096 actor-recipient deliveries per flush (including broadcast recipients). Overflow logs and drops the remaining work. A recursive flushMessages call is a no-op; the outer flush owns delivery. Queues retain capacity.

Each broadcast snapshots registered listeners when delivery begins. Adding or removing listeners during a handler cannot skip a different recipient or add a new recipient to the current broadcast. Dead/inactive targets are checked again before invocation. Nested immediate broadcasts have independent snapshots. Clearing queues cancels the current drain; newly enqueued messages survive for the next flush.

Engine has immediate sendNow / emitNow for rare re-entrancy; game modules use deferred send / emit only.

Zig send / receive

Preferred — event helpers with self as sender:

zig
// ActorContext form (best DX)
e.sendEventTo("msg_door_zig", "open", .{});
e.emitEvent("ping", .{});
e.sendEvent(door, "damage", .{ .args = &.{ .{ .float = 25 }, .{ .entity = other_id } } });

// ActorRef form
const a = hi.actors;
a.sendEventToFrom(id, "msg_door_zig", "open", .{});
a.emitEventFrom(id, "ping", .{});
a.sendEventFrom(id, door, "damage", .{ .args = &.{ .{ .float = 25 } } });

When you must set from explicitly (or leave it empty), use sendEvent / sendEventTo / emitEvent with Event{ .from, .args }.

Explicit Message still works when you need full control:

zig
var open = hi.Message.init(a.internMessageName("open")).withFrom(id);
a.send(a.find("msg_door_zig"), open); // dead → no-op
a.sendToSceneId("msg_door_zig", open);
a.emit(hi.Message.init(a.internMessageName("ping")).withFrom(id));

Receive on a logic component (defineActor discovers onMessage → emit listener):

zig
pub fn onMessage(self: *@This(), _: hi.ActorContext, msg: *const hi.Message) void {
    if (a.messageNameEql(msg.name, "open")) {
        self.open = true;
    }
}

Kawa send / receive

kawa
Actors.send("msg_door_zig", "open");
Actors.send(door, "damage", 25);
Actors.emit("ping");

fn on_message(name, from, a0, a1, a2, a3) {
    if (name == "open") { /* ... */ }
    if (name == "ping") { Debug.log("ping"); }
}

from is an actor handle (or empty). Hikari disables Kawa’s standalone Events.* bus and its events_* aliases with the VM option enable_events = false. Use Actors.* for gameplay messages; both languages share World delivery rules.

Delivery fan-out

When a message is delivered to a live active actor:

  1. Zig — each archetype component that declares onMessage (declaration order).
  2. Kawa — every script slot on that actor that declares on_message (multi-slot component scripts + entity-level script).

Both languages can handle the same event on one actor. Script-only actors register as listeners when on_message is present at attach; Zig defineActor registers when any component has onMessage.


Component field access

Cross-actor field access uses exact typed helpers in Zig and dynamic name-keyed helpers in scripts and tooling. Same rules apply to self and other actors. See Gameplay API for types, borrowing, and runtime state.

Zig — prefer target-first component access

Every defineComponent type exposes weak-handle field access without repeating the component name string:

zig
// After: const Health = hi.defineComponent(.{ .name = "health", .data = struct { hp: f32 = 100 } });

if (target.component(Health).has()) {
    const hp = target.component(Health).get(.hp) orelse return;
    _ = target.component(Health).set(.hp, hp - 10);
    // arrays / vectors:
    // target.component(Health).getAt(.tint, 1);
    // target.component(Health).setAt(.tint, 1, 0.5);
}

// Runtime field path (rare):
_ = hi.actors.getNumber(target, Health.component_name, "hp", 0);
_ = hi.actors.setNumber(target, Health.component_name, "hp", 0, 50);
HelperRole
id.component(Comp).has()Component present on live actor
target.component(Comp).get(.name) / set(.name, value)Exact declared field value (including arrays, strings, assets)
target.component(Comp).getAt(.name, index) / setAt(.name, index, value)Array / vector element
target.component(Comp).getPath("nested.path") / setPath("nested.path", value)Same typed accessor for a compile-time nested or computed path
hi.actors.getNumber / setNumberRuntime path string

Generic free functions remain for dynamic names:

ZigKawaNotes
hasComponent(id, "health")Actors.has(actor, "health") → boolComponent on mask
getNumber(id, "health", "hp", 0)Actors.get_number(…)?f64 / number | nil
setNumber(id, "health", "hp", 0, v)Actors.set_number(…)bool / 0|1; fires onChanged when applicable
  • Types: float, int, bool (0/1), enum ordinals; vectors/arrays use index (0 for scalars).
  • Also: strings (getString / setString) and assets (getAsset / setAsset / Kawa get_asset / set_asset).
  • Not supported: arbitrary method call on another actor.
  • Missing / dead / bad field: soft fail (null / false / nil).
  • Fields come from Zig Data (property_fields) — scripts never declare inspector schema.
kawa
if (Actors.has(target, "health")) {
    let hp = Actors.get_number(target, "health", "hp");
    Actors.set_number(target, "health", "hp", hp - 10);
    // vector element: Actors.get_number(actor, "fx", "tint", 1)
}

Same-actor typed Zig (ActorContext)

When a lifecycle callback takes ActorContext (or you already hold typed component types on the same actor), prefer:

zig
pub fn update(self: *@This(), e: hi.ActorContext, ctx: *const hi.TickContext) void {
    _ = .{ self, ctx };
    if (e.get(Health)) |h| {
        h.hp -= 1;
    }
    const nav = e.require(Navigation); // archetype-required
    _ = nav;
}
ActorContext.get / requirehi.actors.getNumber
TypingCompile-time component typeName string
Same actor hot pathPreferredFine
Other actorN/A (batch view is self)Use actors.*
KawaN/AUse Actors.*

API quick reference

Zig hi.actors

FunctionRole
find / findByNameScene id / display name → ActorRef
tryFind / tryFindByNameSame + live → ?ActorRef
isAlive / isActiveLiveness / entity-active
internMessageName / messageNameEqlMessage names
send / sendToSceneId / emitMessage bus (explicit Message)
sendEvent / sendEventTo / emitEventName + full Event{ .from, .args }
sendEventFrom / sendEventToFrom / emitEventFromPreferred with self id — from first, then Event{ .args } only
hasComponent / getNumber / setNumberDynamic name-keyed fields

Zig ActorContext message helpers

MethodRole
e.sendEvent(to, name, event)Point-to-point; from = e.id
e.sendEventTo(scene_id, name, event)By scene id; from = e.id
e.emitEvent(name, event)Broadcast; from = e.id

event.from is ignored on these paths — only event.args matter. Empty payload: .{}.

Zig defineComponent field helpers

FunctionRole
id.component(Comp).has()Present on actor
target.component(Comp).get(.hp) / set(.hp, value)Exact typed value access
target.component(Comp).getAt(.tint, index) / setAt(.tint, index, value)Indexed access
hi.actors.getNumber / setNumberRuntime field path

Kawa Actors.*

FunctionRole
find / find_by_nameHandle or nil
is_alive / is_activeBooleans
send(target, name, …args)Target = handle or scene-id string
emit(name, …args)Broadcast
has / get_number / set_numberComponent fields
get_string / set_string / get_asset / set_assetString / AssetRef fields
has_tag / add_tag / remove_tagCatalog gameplay tags

Related (not the unified namespace)

APIRole
Actor.* (Kawa)This scripted actor: transform, scene id, …
World.find / World.sendOlder aliases; prefer Actors.*
Entity.is_alive / Entity.sendOlder aliases

Comparison

MessagesActors field accessHost-only typed call
Cross-languageYesYesNo
Loose couplingHighComponent + field namesNeeds Zig type
Hot pathEnqueue + flushDense property lookupFunction call
Use forEvents, doors, damage, pingshp, flags, knobsInternal host tools only

Demo scene

src/games/example/scenes/messaging.json:

ActorPath
msg_door_zigZig onMessage (blue door)
msg_door_kawaKawa on_message (pink door)
msg_padZig collision → send open to both doors + emit ping
msg_weightDynamic cube falls onto pad
msg_listen_zig_* / msg_listen_kawaMulti-listener ping

Load the messaging layer from the sample session HUD, or set the scene as startup. Both doors start closed; the weight hits the pad and both lift.


Implementation map

ConcernLocation
Game APIsrc/hikari/sdk/src/actors.zig
Engine field accesssrc/hikari/src/scene/world/world_actors.zig
Host ABIWorldApi.component_* + host_bind/world_api_actors.zig
Message queue / flushscene/events/messages.zig, exposed through scene/event_dispatcher.zig
Delivery fan-outscene/event_dispatcher.zig typed emit(.message) (Zig then Kawa slots)
Kawa nativesscene/scripting/kawa_host/actors.zig

Performance notes

  • Send/emit: append to a retained-capacity queue; no per-message heap.
  • Names: intern once; compare with MessageName / messageNameEql.
  • Refs: two-word generation check; dead targets dropped at flush.
  • Broadcast: listeners only (handlers registered), not the full world.
  • Field access: linear scan of a component’s small property_fields list; no heap on the hot path.
  • Budget: each flush caps passes and actor-recipient deliveries, including broadcast recipients; overflow logs and drops.

Keep messages small (name + ≤4 args). Large state lives on components, not in the payload.

See also

  • Tutorials — First messages
  • Game-facing refs — ActorRef weak semantics
  • Scenes and gameplay — components, active trio
  • Scripting with Kawa — attach scripts, tick order
  • Lifecycle — when messages flush
PreviousScenes and gameplayNext Game-facing refs

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/entity-messaging.md
On this pageShort path (preferred)Choose the right channelConceptsFind and livenessZigKawaMessagesFrame orderZig send / receiveKawa send / receiveDelivery fan-outComponent field accessZig — prefer target-first component accessSame-actor typed Zig (ActorContext)API quick referenceZig hi.actorsZig ActorContext message helpersZig defineComponent field helpersKawa Actors.*Related (not the unified namespace)ComparisonDemo sceneImplementation mapPerformance notesSee also Back to top