# Tutorial: first session services

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](/docs/systems/session-services.md).

## 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):

```zig
// hikari-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):

```zig
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](/docs/systems/session-services.md#readiness-stages-show-the-scene-gate).

**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](/docs/systems/session-services.md#session-owned-resources). Play/Edit rules: [Play, Edit, and scenes](/docs/tutorials/play-edit-and-scenes.md).

Sample implementation: `hikari-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):

```zig
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](/docs/tutorials/first-runtime-spawn.md).

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

1. Rebuild and Play.
2. Use the sample HUD “Next Scene” (or your own `hi.world().requestSceneLoad`) and confirm subsystem fields persist.
3. Stop in the editor — the **document** scene returns; session services tear down only on session shutdown.

## Next

- [First UI](/docs/tutorials/first-ui.md) — draw the HUD
- [Play, Edit, and scenes](/docs/tutorials/play-edit-and-scenes.md) — load rules and Stop semantics
