An entity is an archetype (string id) plus a tuple of components. Engine capabilities (hi.ComponentRender, hi.ComponentPhysics, …) and game logic (defineComponent) are listed together. The build recursively imports every .zig under the project src/ into content_manifest.gen.zig; comptime registration picks out defineActor / defineComponent types — you do not maintain a hand registry, and files may live in any subdirectory.
Deep reference: Scenes and gameplay, Scripting with Kawa. Design of the open model: Entity-component model.
Setup checklist
- Add a
.zigfile under the projectsrc/(any folder;entities/is conventional, not required). - Export a
defineActorvalue with a unique.archetypestring (and anydefineComponenttypes the archetype needs). - Rebuild the game/editor so
content_manifest.gen.zigpicks the file up. - Place an actor in scene JSON with the same
"archetype"and a"components"map (scene document"version": 1). - Cameras look local −Z. Identity rotation at +Z looks at the origin; yaw 180 at +Z looks away. Copy
scenes/messaging.json([0, 5, -14], yaw180) — see coordinate space. - Press Play in the editor (or run the standalone game) to run component
update/ scripts.
If the archetype is missing from the manifest, scene load soft-fails to a _missing host instead of aborting.
Components listed by an exported entity may remain private declarations: defineActor
publishes their types to content discovery, which registers them before the
archetype. Export a component separately only when other source files need to
import it or when it is not referenced by an entity (for example, a purely
instance-addable component).
Zig-only
Minimal marker (sample: empty-style archetypes):
const hi = @import("hikari_game");
pub const BeaconEntity = hi.defineActor(.{
.archetype = "beacon",
});With engine capabilities and game logic as components:
const hi = @import("hikari_game");
const BeaconLogic = hi.defineComponent(.{
.name = "beacon_logic",
.data = struct {
speed: f32 = 1,
state: struct { spin: f32 = 0 } = .{},
pub fn update(self: *@This(), _: hi.ActorContext, tick: *const hi.TickContext) void {
self.state.spin += self.speed * tick.dt;
}
},
});
pub const BeaconEntity = hi.defineActor(.{
.archetype = "beacon",
.components = .{ hi.ComponentRender, BeaconLogic },
});Components default to .attachable: they work in archetypes and in the editor's
Add Component menu. Use explicit .embedded only for fixed composition; it has
the same authored fields but cannot be added to arbitrary actors. Populations
without actor identity use defineSwarm. See Gameplay API.
Optional lifecycle methods on the logic component data struct: awake, start, update, lateUpdate, fixedUpdate, onCollision, onMessage, onChanged, onDestroy, deinit.
Cross-actor talk and name-keyed fields: hi.actors / Actors.*. Same-actor typed siblings in Zig: ActorContext.get / require. See Actor communication and First messages.
Built-in capability components (engine):
| Symbol | Capability |
|---|---|
hi.ComponentRender | Mesh / material draw |
hi.ComponentPhysics | Body / collider |
hi.ComponentCamera | Camera |
hi.ComponentLight | Light |
hi.ComponentAudio | Audio source |
hi.ComponentVisualZone | Visual zone |
hi.ComponentTransform | Explicit transform (always present on live entities; rarely listed) |
Scene actor (com.hikari.scene document version 1). Capabilities live under "components", not top-level "render" / "physics":
{
"id": "beacon_01",
"name": "Beacon",
"archetype": "beacon",
"transform": {
"position": [0, 1, 0],
"rotation_euler": [0, 0, 0],
"scale": [1, 1, 1]
},
"components": {
"render": {
"mesh": "asset://./models/cube",
"material": "asset://./materials/cube"
}
}
}Scene document root uses numeric "version": 1, with packs and actors at the top level. Shinra cooks .shinscene from the same form.
Sample Zig actors to copy: floor_entity.zig, cube_entity.zig, disco_light_entity.zig.
Active / enable
Active / enable trio on the live actor (defaults all on):
| Switch | Scene | Runtime |
|---|---|---|
| Entity | "active": false | hi.world().setActive(id, false) — stops update/scripts/messages; gates physics + render |
| Physics | "components": { "physics": { "is_active": false } } | hi.world().setPhysicsActive(id, false) — body out of sim |
| Render | "components": { "render": { "is_visible": false } } | hi.render().setVisible(id, false) — skip draw |
Full rules: Scenes and gameplay — Active / enable trio.
Kawa-only (script-primary)
Declare capabilities and a default script; put game logic in the script only:
const hi = @import("hikari_game");
pub const BannerEntity = hi.defineActor(.{
.archetype = "banner",
.components = .{hi.ComponentRender},
.script = "asset://./scripts/banner",
});Script (assets/scripts/banner.kawa — extension-free stem in the ref):
fn start() {
Debug.log("banner ready: " + Actor.get_scene_id());
}
fn update(dt, total_time) {
_ = dt;
_ = total_time;
}Entity script hooks: start, update, late_update, on_collision, on_message. Scripts do not run in Edit mode — enter Play.
Sample: message_kawa_door_entity.zig + assets/scenes/messaging/scripts/message_door.kawa.
A scene actor may also override the script without changing the archetype:
"script": { "path": "asset://./scripts/collision_logger" }(cube_00 in scenes/physics_playground.json does this.)
Scripted component
A script can also live on a component instead of the archetype. The component keeps typed Zig fields (inspector-editable, serialized under "components"), and the script sees them as data:
const hi = @import("hikari_game");
pub const Patrol = hi.defineComponent(.{
.name = "patrol",
.script = "asset://./scripts/patrol",
.data = struct {
speed: f32 = 2.0,
radius: f32 = 12.0,
},
});
pub const GuardEntity = hi.defineActor(.{
.archetype = "guard",
.components = .{ hi.ComponentRender, Patrol },
});One script slot attaches per component instance; an actor with two scripted components plus an entity .script has three slots. A scene actor overrides a component's script inside that component's block:
"components": {
"patrol": { "speed": 3.0, "script": { "path": "asset://./scripts/patrol_aggressive" } }
}Engine capability builtins and .swarm components reject .script at comptime. Detail: Script logic authoring. Working sample: src/games/example/scenes/script_interop.json (reactor = Zig data + component script + entity script on one actor).
Mixed Zig + Kawa
Engine builtins already provide bare empty / light / camera. Genre cameras layer logic + script on a new archetype name (sample fly_camera):
const hi = @import("hikari_game");
const FlyCameraLogic = hi.defineComponent(.{
.name = "fly_camera_logic",
.data = struct {
move_speed: f32 = 14,
// user_data schema fields as needed…
},
});
pub const FlyCameraEntity = hi.defineActor(.{
.archetype = "fly_camera",
.components = .{ hi.ComponentCamera, FlyCameraLogic },
.script = "asset://./scripts/camera_controller",
});Scene user_data / component properties seed Zig fields; Kawa can read the same data. Do not redefine host builtins (empty / light / camera / visual_zone).
| Mode | Logic component(s) | .script | Authored schema |
|---|---|---|---|
| Zig-only | yes | no | from component metadata.properties |
| Hybrid | yes | yes | from component metadata |
| Script-primary | no | yes | none (or inspector-only) |
| Scripted component | yes (defineComponent.script) | per component | from that component's fields |
Verify
- Rebuild:
bin/kaji/kaji editor --workspace=$PWD --project=$PWD/src/games/example --type=dynamic --config=debug --run(or your usual product command). - Open the scene, confirm the actor appears in the hierarchy.
- Enter Play — component
updateand Kawa hooks run; entity timing (Profiler → Entity timing) can confirm costs.
Next
- Cross-scene HUD / state → First session services
- Draw widgets from an entity → First UI
- Wire a key → First input action