The engine has no “global entity” archetype. Cross-scene state and HUD belong on the optional GameSubsystem, which outlives scene replace. Scene JSON is for placeable content only.
Deep reference: Session services.
What people mean by “global”
| Intent | Correct seam |
|---|---|
HUD / progress / settings that survive Scene.load | GameSubsystem (+ optional session Kawa script) |
| Runtime actor not bound to an additive layer | w.spawn(&.{ .layer = .loose, … }) — shows under Play hierarchy Global group |
| Fields not editable in the inspector | behaviour metadata = .{ .runtime_only = true } (still placeable in scenes) |
Do not duplicate a “GameManager” actor into every scene JSON.
Minimal GameSubsystem
Export from the game root (sample already does):
// src/games/example/src/root.zig
pub const config = @import("config.zig").config;
pub const content_manifest = @import("content_manifest.gen.zig");
pub const GameSubsystem = @import("session.zig").GameSubsystem;Skeleton (hooks are optional beyond create / destroy; call sites use hi.world() / hi.ui() — do not store a World pointer):
const std = @import("hikari_std");
const hi = @import("hikari_game");
pub const GameSubsystem = struct {
allocator: std.mem.Allocator,
score: i32 = 0,
pub fn create(allocator: std.mem.Allocator) !*GameSubsystem {
const self = try allocator.create(GameSubsystem);
self.* = .{ .allocator = allocator };
return self;
}
pub fn destroy(self: *GameSubsystem) void {
self.allocator.destroy(self);
}
pub fn onLaunch(self: *GameSubsystem) void {
_ = self;
// Register one-time session providers/resources.
}
/// Runs once per World — the initial one, and every disposable Play copy.
/// One subsystem instance serves them all, so key per-World state by
/// `scope.id` instead of assuming there is only ever one.
pub fn onWorldAttach(self: *GameSubsystem, scope: hi.WorldScope) void {
_ = self;
_ = scope;
// Reserve UI capacity and install world-local script natives.
}
pub fn onTick(self: *GameSubsystem) void {
_ = self;
// Draw cross-scene HUD; tick session Kawa if attached.
// hi.world().updateSessionScript();
}
pub fn onSceneLoaded(self: *GameSubsystem, info: hi.SceneInfo) void {
_ = self;
_ = info;
}
pub fn onSceneWillUnload(_: *GameSubsystem) void {}
pub fn onTerminate(_: *GameSubsystem) void {}
};Useful hooks: onLaunch / onTerminate for the session, onWorldAttach / onWorldDetach for each initial or disposable Play World, onTick, onPlayEnded (editor Stop — clear play-ephemeral HUD here, not in unload), onSceneWillUnload / onSceneLoaded, onSceneLayerLoaded / onSceneLayerReady (a manual layer is loaded but not yet shown) / onSceneLayerWillUnload, onSceneReadyStage (none → entities → assets → gpu → world all).
onSceneLoaded means actors spawned only. For a loading screen, wait until hi.world().sceneLoadSnapshot().ready_stage == .all (or handle onSceneReadyStage), then fade out. Stage table: Session services — readiness.
Editor Stop is immediate because the authoring world is retained while a disposable Play world runs. Stop switches back before returning, then retires Play invisibly in bounded slices. Keep expensive shared resources on the subsystem — session-owned resources. Play/Edit rules: Play, Edit, and scenes.
Sample implementation: src/games/example/src/session.zig + session_ui.zig (session card, settings, Next Scene / Reload).
Session Kawa script
Attach once the asset store is ready (sample retries from onTick until bytecode is live):
const std = @import("hikari_std");
const hi = @import("hikari_game");
const w = hi.world();
const script = hi.AssetRef.must(.script, "asset://./scripts/session");
w.attachSessionScript(script) catch |err| {
std.log.info("session script: {any}", .{err});
};Session scripts have no Actor.* — they are not entity scripts. Tick them from onTick via hi.world().updateSessionScript() (no-op outside a play tick; see sample).
Loose runtime spawn (optional)
When you need a live actor that is not authored in the current scene and should not die with an additive layer unload, call hi.world().spawn with .layer = .loose — see First runtime spawn.
Replace scene unload still tears down live world entities; only GameSubsystem state survives. For HUD that must persist across replace, draw from onTick, not from a scene actor.
Verify
- Rebuild and Play.
- Use the sample HUD “Next Scene” (or your own
hi.world().requestSceneLoad) and confirm subsystem fields persist. - Stop in the editor — the document scene returns; session services tear down only on session shutdown.
Next
- First UI — draw the HUD
- Play, Edit, and scenes — load rules and Stop semantics