# Actor and component lifecycle

`hi.defineActor` composes component types. It does not accept actor-level
lifecycle functions: implement optional `pub fn` hooks on a component's `Data`
struct and put that component in the actor's `.components` tuple. Actor events
fan out to those components in tuple order. Both `.embedded` and `.attachable` components participate, including runtime
attachments. Extra attachments receive events after the declared tuple. Runtime tick membership uses component/archetype batch lookup and a reverse row index, so enrollment and removal do not scan the population. Callback retirement queues generation handles until the borrow ends; swap removal repairs the moved member's reverse index.

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

pub const LampLogic = hi.defineComponent(.{
    .name = "lamp_logic",
    .requires = .{hi.ComponentLight},
    .data = struct {
        pub fn onEnable(_: *@This(), actor: hi.ActorContext) void {
            actor.render().setLight(actor.id, .{ .enabled = true });
        }

        pub fn onDisable(_: *@This(), actor: hi.ActorContext) void {
            // Release gameplay subscriptions or other activation-scoped state.
            // The actor handle may already be invalid during teardown.
            _ = actor;
        }
    },
});

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

The engine already gates lighting, rendering, physics, and ticking by actor
activity. Hooks are for gameplay reactions; they are not required to hide an
inactive light or stop an inactive actor's updates.

## Hooks and dispatch locations

Every hook below takes `self: *Data`. Except `deinit`, ordinary hooks then take
`actor: hi.ActorContext`. The table lists any remaining arguments. All return
`void`, except `awake`, which may also return an error union of `void`.

The regular callback path is **World/scene operation → unified event dispatcher →
host bridge → generated component handlers**.
[`event_dispatcher.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/event_dispatcher.zig) owns
actor lifecycle, typed events, native tick admission to batches, Kawa slot
delivery, and Kawa session notifications. `Entity` convenience methods alias
these dispatcher entry points; they contain no separate dispatch logic.
The host bridge is
[`game_archetype.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/game_api/game_archetype.zig); the
component fan-out and update runners are generated in
[`define_actor.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/sdk/src/define_actor.zig). Hook signatures
are checked by [`declaration_validation.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/sdk/src/declaration_validation.zig).

| Hook | Remaining arguments | When dispatched | Dispatch origin |
|---|---|---|---|
| `awake` | `?*const hi.SceneActorView` | Instance initialization, including inactive and withheld actors; scene view is optional | [`scene/world/scene/spawn.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/scene/spawn.zig) `spawnByArchetypeWith` and [`scene_loader/instantiate.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/scene_loader/instantiate.zig) → `Entity.instantiate` → bridge `onInstantiate` |
| `start` | None | Once per actor when the spawn/load path starts it; authored-inactive actors still start, but withheld layers defer startup until presentation | [`scene_loader/spawn.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/scene_loader/spawn.zig), [`scene_loader/instantiate.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/scene_loader/instantiate.zig), [`scene/world/scene/layer.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/scene/layer.zig) `presentSceneLayer` → `Entity.start` → bridge `onStart` |
| `onEnable` | None | After startup when effectively active, then each effective inactive → active transition | [`event_dispatcher.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/event_dispatcher.zig) `syncActivation`, reached after `start`, [`world_entities.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_entities.zig) activity/presentation setters, and [`scene/world/scene/edit.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/scene/edit.zig) `applySceneActorActive` → bridge `onEnable` |
| `onDisable` | None | Effective active → inactive transition, or final teardown if enabled was previously delivered | The same `syncActivation` path, plus `Entity.terminate` from [`world_entities.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_entities.zig) `queueEntityForCleanup` → bridge `onDisable` |
| `update` | `*const hi.TickContext` | Active actors in the archetype's tick group, subject to component cadence | [`world_tick.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_tick.zig) `runTickGroup` → dispatcher `runPhase(.update)` → bridge `updateBatch` → generated component tile runners |
| `lateUpdate` | `*const hi.TickContext` | Active actors in `.post_update`; component `tick_rate` applies only to `update` | `world_tick.zig` `runTickGroup(.post_update, ...)` → dispatcher `runPhase(.late_update)` → bridge `lateUpdateBatch` → generated runners |
| `fixedUpdate` | `*const hi.TickContext` | Active actors during admitted fixed steps; requires a nonzero fixed timestep | `world_tick.zig` `runFixedUpdateSteps` → dispatcher `runPhase(.fixed_update)` → bridge `fixedUpdateBatch` → generated runners |
| `onCollision` | `*const hi.CollisionInfo` | Contact events for active actors; `phase` distinguishes `enter`, `stay`, and `exit` | [`world_entities.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_entities.zig) `dispatchCollisionEvents` → `Entity.onCollision` → bridge `onCollision` |
| `onAnimationEvent` | `*const hi.AnimationEvent` | Authored clip markers after animation evaluation, for active actors | `world_tick.zig` `dispatchAnimationEvents` → `Entity.onAnimationEvent` → bridge `onAnimationEvent` |
| `onMessage` | `*const hi.Message` | Messages delivered to active, non-terminating actors; Zig handlers precede Kawa script slots | [`events/messages.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/events/messages.zig) `flushMessages` / immediate delivery → `Entity.deliverMessage` → bridge `onMessage` |
| `onChanged` | `[]const u8` | Explicit property notifications, including successful typed/dynamic field writes and inspector data edits; inactive actors can receive them | [`world_actors.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_actors.zig) field setters and `scene/world/scene/edit.zig` data edits → `Entity.userDataChanged` → bridge `onUserDataChanged` |
| `onEnvironmentPreview` | `hi.EnvironmentPreview` | Transient editor environment preview for active supporting actors; restore authored state when `active` becomes false | [`world_environment_preview.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/world/world_environment_preview.zig) `applyEnvironmentPreview` → `Entity.applyEnvironmentPreview` → bridge `onEnvironmentPreview` |
| `onDestroy` | None | Teardown while component storage is still available, after any final `onDisable` | `world_entities.zig` `queueEntityForCleanup` → `Entity.terminate` → bridge `onTerminate` |
| `deinit` | `std.mem.Allocator` instead of actor context | Release owned row data; also used when a custom scene decoder replaces an existing value | Generated [`define_component/scene.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/sdk/src/define_component/scene.zig) `deinitRow` / custom decoder for columns; `define_actor.zig` `destroyBehaviour` for embedded data |

See [Tick groups](/docs/systems/lifecycle.md#tick-groups) for the complete frame
ordering, animation phase, fixed-step admission, and cadence rules.

## Dispatch cost and measurement

Tick callbacks use generated lists containing only components that implement the
phase. The host processes 512-entry tiles, with component-major loops and native
Zig calls into component methods. Detailed per-entity timing deliberately reduces
batching; disable it when measuring normal dispatch throughput.

Activation is transition-driven, with no per-frame polling. Actors without
activation hooks have null activation vtable slots. A single participating
component needs no fan-out lifetime check; multiple components retain checks
between callbacks so one retiring the actor prevents later delivery.

From `src/hikari`, run the CPU-only activation benchmark:

```sh
zig build entity-bench -Dgame-src=<absolute-game-src> -Doptimize=fast -Dprofiler-timing=false -- --case=activation --json
```

Pass `-Doptimize=fast` explicitly so imported SDK modules use the same
optimization as the benchmark executable. This case exercises the real host/SDK
bridge with 10,000 actors and zero, one, or five embedded callback components.
It reports notification-only cost separately from public `setActive` transitions,
which include handle resolution and active-list maintenance. Each callback
increments a checked counter. It excludes rendering, physics, attachable-column
access, dynamic-library boundaries, and substantial user callback work; results
are not whole-game frame costs or a comparison with another engine.

## Unified dispatcher contract

All actor/component event producers enter `scene/event_dispatcher.zig`. Its
specialized paths share one policy boundary without boxing events or adding a
runtime event-name switch to each actor update:

```zig
// Engine-internal API. Gameplay keeps ActorContext / hi.actors / Actors.*.
dispatcher.emit(.message, entity, &message);
dispatcher.queueCollision(world, entity, contact);
dispatcher.flushCollisions(world);
dispatcher.syncActivation(entity);
dispatcher.runPhase(.update, world, descriptor, entries, tick, true);
```

- **Ownership:** `World.events` owns message queues, collision/animation queues,
  and recipient scratch storage. Edit and Play Worlds have separate state.
  Capacity is retained between deliveries and released at World teardown.
  First use or a larger burst can grow a queue; steady-state delivery does not
  allocate one object per event.
- **Typed events:** `emit` selects payload type and handler at compile time.
  Collision, animation, message, and preview require a live active actor.
  Property notifications also reach inactive actors. Startup and teardown use
  dedicated entry points because initialization/teardown storage can exist
  without a resolvable live actor handle.
- **Lifetime:** deferred targets and collision counterparts are generation-checked
  references, never retained actor pointers. Animation names are copied. Queues
  copy the current event before invoking callbacks, so reentrant appends cannot
  invalidate its payload. Native component fan-out stops if a preceding callback
  retires the actor; teardown intentionally retains access to component storage.
- **Languages:** Zig runs before Kawa. Script recipients are captured before Zig
  delivery, using actor handles plus monotonically assigned attachment IDs.
  Removed/replaced slots are skipped, even if allocator addresses are reused;
  slots attached during a delivery join subsequent events. Script tick phases
  snapshot their slot recipients with the same identity checks.
- **Field edits:** numeric component edits update only that component's Kawa
  `data` object; actor user-data edits update only the actor script. An absent
  owner script is skipped, without falling back to another attachment.
- **Ordering:** component declaration order, then script attachment order.
  Collision and animation flushes consume the queue present at entry. Events
  appended during delivery wait for the next flush; recursively flushing that
  same queue is a no-op. Clearing queues cancels the current drain. Messages use
  the bounded multi-pass contract in [Actor communication](/docs/systems/entity-messaging.md#frame-order).
- **Batches:** World still selects tick groups, fixed steps, and LOD policy;
  the dispatcher invokes the admitted batches. Generated native component tiles
  stay intact. The worker pool in `src/dispatch/` is a separate scheduling service.
- **Execution:** event producers and queue mutation run on the game thread after
  worker barriers. User callbacks run outside World locks. Parallel components
  retain their restricted batch/command-buffer interface; they cannot emit
  ordinary events directly from worker threads.

Kawa session update, scene/layer readiness notifications, and script startup and
teardown also enter this dispatcher. Hikari disables Kawa's standalone `Events.*` bus and its `events_*` aliases with
the VM creation option `enable_events = false`. Cross-actor messages use `Actors.send` / `Actors.emit` and share the
same World dispatcher as Zig. Component `deinit` remains storage cleanup (including
scene decoding replacement), not a broadcastable gameplay event.

## Activation and teardown contract

Startup happens once; activation can repeat. The actor's declared components
share this activation cycle. Destruction exits the cycle, as described below.

```mermaid
flowchart LR
    init["Initialize · awake"] -->|start once| inactive["Not enabled"]
    inactive -->|onEnable| active["Active"]
    active -->|onDisable| inactive
    class active accent
```

`start` remains a one-time initialization hook, including for authored-inactive
actors. `onEnable` follows `start` and runs only if the final state is active.
Changing active state during `awake` or `start` does not emit premature activation
callbacks. Re-enabling an actor does not rerun `start`.

Effective activity is **authored actor active AND layer presented**. There is no
inherited parent-activity gate. Setting the same value again produces no extra
activation event. Changing authored activity while a layer is withheld produces
no event until effective activity changes after startup. Presenting a layer
updates all its actors' effective state before starting/notifying them.

Public activity setters and editor Active edits dispatch synchronously after
updating subsystem state and releasing the World lock. Notification state is
committed before callbacks, so writing the same state from a hook is harmless;
opposite-state writes can synchronously reenter activation hooks. The internal
`setEntityActiveLocked` / `setEntityPresentedLocked` functions only update state;
callers finish activation dispatch after unlocking.

Final `onDisable` runs at teardown, immediately before `onDestroy`, if the actor
was enabled after startup and has not since been disabled. Never-started or
never-enabled actors receive no synthetic enable/disable pair. Deferred destruction
marks the handle dead first and delivers teardown callbacks during cleanup.

During teardown, component storage remains available through `ActorContext.get`
/ `require`, but `actor.id` is historical: world lookups and target-bound field
operations may fail. Kawa slots have already been destroyed before final Zig
`onDisable` / `onDestroy`. `deinit` is storage cleanup and must not depend on a
live actor, sibling pointers, scripts, or world services. Custom scene decoding
can invoke it outside actor destruction.

## Dynamic membership

`ActorRef.component(C).add()/remove()` and `C.addTo(actor)/removeFrom(actor)` enter
`event_dispatcher.changeComponent`. The dispatcher owns validation, deferred
structural commands, and component lifecycle delivery in
[`events/components.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/events/components.zig).
[`components/mutation.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/src/scene/components/mutation.zig)
handles storage and dependency enforcement; scene construction uses this storage
path before the normal `awake`/`start` sequence. Inspector live additions use the
same public dispatcher path as gameplay.

Adding to an initialized actor calls the new component's `awake` with a null scene
view, then `start` if the actor has started, then `onEnable` if enabled. A withheld
actor postpones startup/enable until presentation. Removal calls only that
component's final `onDisable` and `onDestroy` before reclaiming storage. The actor
and its remaining components keep running. Removing a component from its own
callback is deferred; callback borrows remain valid. If a removal hook retires its
actor, the retiring component is excluded from the actor's teardown fan-out so
hooks are not delivered twice. Changes to actor activity during a new
component's `awake` or `start` affect existing components; activation of the new
attachment waits until its own startup completes.

Extra components use generated independent callbacks and tick kernels in
[`define_component/callbacks.zig`](https://github.com/Flying-Rat/hikari/blob/develop/src/hikari/sdk/src/define_component/callbacks.zig).
Tick membership is indexed by component and actor archetype. The dispatcher runs
512-entry tiles using the existing parallel runners and command buffers, without
scanning all actors to discover attachments each frame. `update` follows the
owning archetype's tick group and the component's `tick_rate`; `lateUpdate` uses
post-update and `fixedUpdate` uses the fixed phase. Declared attachable components
keep the original generated archetype batches when removed and restored. Message
and environment-preview listener rosters are updated on membership changes.

Extra-component batches run after the declared native batches in their phase;
they are not merged into a global component-priority order. Do not rely on tuple
ordering between an extra component and a declared sibling. Express ordering
through tick phases/groups or explicit coordination. Structural changes can
allocate and scan the target's pending commands; they are not allocation-free
field writes. The repeated native tick path retains batching rather than doing
per-frame name lookup or discovering components by scanning the whole World.

Structural commands are FIFO and generation-checked. World ticks (also paused
World ticks), tick groups, and fixed phases flush them outside callback borrows
and World locks. An actor retired within a callback keeps its storage until the
borrow ends and normal cleanup can purge it. Each flush processes one queue snapshot, so hooks cannot create
an unbounded recursive structural loop. Physics mutation acquires the scene
barrier before World locks; callbacks run unlocked. System removal shares the
entity teardown policies for GPU/audio/physics/assets. Component-owned Kawa slots
and pending script attachments are removed with their component.

The [runtime API and storage limits](/docs/guides/gameplay-api.md#runtime-component-membership)
include committed-state reads, errors, and initialization timing. These are
structural changes; ordinary data/field writes remain immediate.

## Component scope and current limits

- Ordinary game components do not have independent enabled bits. The new hooks
  follow their owning actor. Built-in light `enabled`, render `is_visible`, and
  physics `is_active` switches affect those systems and do not emit actor hooks.
- `onChanged` is an actor-wide fan-out carrying a property path, without a
  component identity. It is a write notification, not an equality-based change
  detector. Direct mutation such as `actor.require(Health).hp = 100` does not
  emit it. There is no additional `changed` or `onActiveChanged` hook.
- Parallel components allow `update`, `lateUpdate`, and `fixedUpdate` with
  `hi.ParallelEntry`, plus data cleanup. Ordinary activation/event hooks are rejected.
- Swarms have bulk `integrate` or per-row `update`, and data cleanup; no actor
  lifecycle hooks. Kawa script callbacks are a separate surface documented in
  [Scripting with Kawa](/docs/guides/scripting-kawa.md); these activation hooks are Zig hooks.

`awake` accepts errors in its signature, but the current `Entity.instantiate`
wrapper escalates an initialization error to a panic. Recoverable per-actor
initialization failure is not yet part of the lifecycle contract.

Scene load/unload and editor Play replacement create/destroy actor instances;
those operations use the existing initialization and teardown hooks. Extra
per-actor `onSceneLoaded`, `onSpawn`, or `onDespawn` aliases are not needed for the
same transitions. Dynamic membership reuses these hooks rather than introducing
separate add/remove event names.
