Drop protocol.md; rewrite the tick-loop note around the game-loop-driven sim and the intent model; rewrite the roadmap (network items removed, sim/game crate layout, save/load and NPC blocking added); fix crate paths in the remaining notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.9 KiB
Executable File
Hybrid Tick Loop
Single 24 Hz base tick; subsystems self-select frequency via stride scheduling. The sim is
headless (sim crate) and advanced tick by tick from the game loop.
Driving the sim from the render loop
Rendering runs at frame rate; the sim runs in whole ticks. Game::update accumulates frame
time and calls Sim::step() once per elapsed TICK_MS (game/src/game.rs):
self.tick_accum_ms += (dt as f32).min(MAX_FRAME_MS);
while self.tick_accum_ms >= TICK_MS {
self.tick_accum_ms -= TICK_MS;
if self.sim.step() { // true when a movement window executed
self.track_lerp(); // entity positions may have changed
self.sync_route(); // click-to-move bookkeeping
self.step_movement(input);// schedule the next step right away
}
}
MAX_FRAME_MS (250 ms) caps how much a single slow frame catches up, so a debugger pause or
window drag drops time instead of spiralling into a burst of ticks. Rendering interpolates
entity positions between tiles over one movement interval (EntityLerp), so the 6 Hz
movement cadence looks smooth at any frame rate.
Stride scheduling
All subsystems hang off one monotonic Sim::tick counter. Each subsystem fires when its
stride condition is met. Current implementation:
tick % 1 == 0 → (game loop) input processing, intent scheduling
tick % 4 == 0 → entity_tick() — movement / collision (sim.rs, 6 Hz)
tick % 8 == 0 → (reserved) slower AI routines, pathfinding refresh
tick % 192 == 0 → (reserved) world simulation (8-sec cycle: weather, daylight, respawns)
24 Hz is highly composite (divisors: 1, 2, 3, 4, 6, 8, 12, 24), giving flexible stride
options without resorting to co-prime tricks. tick >> 3 yields 3 Hz pulses; world-sim
at % 192 is exactly an 8-second cycle.
Why entity_tick runs at 6 Hz, not 12 Hz: without the action-point model (below), one
entity_tick = one tile of movement, so the tick rate is the movement rate. 6 Hz yields the
target ~6 tiles/sec. Once energy/speed are reintroduced, entity_tick can move up to 12 Hz
(% 2) and per-entity speed sets the effective movement rate instead — decoupling sim rate
from movement rate.
Intents: one action per entity per movement window
Actors do not act directly on the world; they schedule an intent (Sim::set_action). The
sim keeps exactly one intent per entity, setting it again replaces it, and executing the
movement window consumes it. So an actor moves at most one tile per window no matter how
often it changes its mind in between, and a tap shorter than a window still registers.
The player's intent is (re)set every frame from held keys or the click-to-move route; NPCs
will use the same call from their think() (roadmap 08). Validation goes through
sim::step_allowed on both sides — the pathfinder plans with the exact function the sim
executes, so a planned route never contains a step the sim will reject.
Action-point model (entity AI)
Status: not yet implemented. Removed from
entity.rsas premature (no NPCs need it yet). Until it returns,entity_tickruns at 6 Hz to set the movement rate directly (see above). Reintroduce alongside roadmap item 08 (NPCs).
Layered on top of entity_tick(). Replaces fixed per-entity cooldown timers.
Each entity carries:
energy: i32— accumulates each tickspeed: u8— added to energy everyentity_tick()
An entity acts when energy >= ACTION_COST, then pays the cost:
// inside entity_tick()
entity.energy += entity.speed as i32;
if entity.energy >= ACTION_COST {
entity.act(&mut world);
entity.energy -= ACTION_COST;
}
Fast entities (speed >= ACTION_COST) act every tick. Slow entities act every N ticks
naturally without any scheduler involvement. Haste and slow effects become simple speed
modifiers — no special-case scheduling needed.