Shared time utilities for gameplay, session services, and host clocks. Implementation lives under src/hikari/sdk/src/temporal/. Exports: hi.temporal plus aliases hi.Timer / hi.Interval / hi.Cooldown / hi.Stopwatch / hi.Deadline / hi.Flow / hi.Timers / hi.nowNs / hi.wallTimeMs / hi.wallTimeSec / hi.wallTimeNs (and the *Io variants).
Goal
Delays, rate limits, intervals, multi-step routines, and clock helpers under hot-path constraints: plain values, no per-frame heap, poll edges — no callback graphs on the hot path.
Non-goals
- Language
async/ stackful coroutines /yieldkeywords (Zig has no built-in suspended iterators; use pollableFlow/Timerinstead). - Host-mediated scene-load waits (use
world.sceneLoadSnapshot/ readiness stages). - Calendar / timezone / wall-clock scheduling.
- Replacing Motion Kit duration/delay fields on tweens.
Timer / Cooldown / Interval cover single delays and rate limits. Flow is the multi-step story: explicit phases on caller-owned state, short update steps, no hidden suspension or heap iterators. Coming from another engine's coroutine model? See Migration.
Where it sits
entity / GameSubsystem state
│
▼
temporal.* (Timer, Interval, Cooldown, Flow, Stopwatch, Deadline, Timers bus)
│
└─► tick with TickContext.dt / total_time each frameWall/mono “now” lives in sdk/src/time.zig (nowNs, wallTimeMs / Sec / Ns, Io variants; backed by std.Io from the hikari_std module). Temporal re-exports those as hi.nowNs / hi.wallTimeMs / … and as hi.temporal.nowNs. Session sim dt stays session-owned Time — do not call Zig std.Io.Timestamp from product code.
Core types
| Type | Role |
|---|---|
Timer | One-shot countdown; after, tick, pulse (finish edge) |
Interval | Repeating period; tick → fire count (catch-up) |
Cooldown | Rate limit; ready / trigger / tick |
Flow | Multi-step routine; wait / waitUntil / waitFrame / onEnter / advance |
Stopwatch | Accumulator; start/pause/unpause |
Deadline | Absolute threshold vs total_time |
Timers | Dense keyed one-shot bus |
Usage
Single delay / cooldown
door_timer: hi.Timer = .immediate(),
fire_cd: hi.Cooldown = .init(0.35),
pub fn update(self: *@This(), _: hi.EntityId, ctx: *const hi.TickContext) void {
if (self.door_timer.pulse(ctx.dt)) { /* open */ }
self.fire_cd.tick(ctx.dt);
if (hi.world().actionPressed("fire") and self.fire_cd.trigger()) { /* shoot */ }
}Multi-step routine (Flow)
const Phase = enum(u32) { windup, sound, wait_player, open, _ };
flow: hi.Flow = .begin(),
pub fn update(self: *@This(), _: hi.EntityId, ctx: *const hi.TickContext) void {
const f = &self.flow;
if (!f.running()) return;
_ = f.wait(@intFromEnum(Phase.windup), ctx.dt, 0.5);
if (f.onEnter(@intFromEnum(Phase.sound))) {
// play sound
f.advance();
}
_ = f.waitUntil(@intFromEnum(Phase.wait_player), playerNear());
if (f.onEnter(@intFromEnum(Phase.open))) {
// open door
f.stop();
}
}Keyed session delays: hi.Timers (after / tick / pulse / cancel).
Files
src/hikari/sdk/src/temporal/
temporal.zig barrel
clock.zig unit helpers
timer.zig Timer, Interval, Cooldown, Stopwatch, Deadline
flow.zig Flow
bus.zig Timers