# The core loop of your game Every GripForge game, on every engine, runs on the same loop. The CLI, the MCP and the hosted creation apply it to each new game and fail a game or a kit that breaks it. The full contract: `gripforge docs game-engine-contract`, §12. ## Three clocks | Clock | Rate | What runs there | |---|---|---| | Game tick | `game.engine` `tick.hz` (5..120 Hz) | All game logic: state, actions, timers, rules (kit `update` / `late_update`, hooks, triggers) | | Physics step | its own fixed step (1/60 s) | Physics only: forces, velocities, contacts | | Render | the display rate | Drawing; poses interpolated between the last two tick snapshots (`alpha`) | Frame / present hooks and physics callbacks only **read** the published state. A heavy system runs every N ticks (`kx.schedule`, `tick.systems`), never through a throttle written by hand. The tick can change during play: `runtime.core.tickAdjust({ hz })`, the config, or the F3 panel. ## The tick of your genre A new game gets its tick explicitly at creation: | Kind of game | tick.hz | |---|---| | cards, collection (turn-paced) | 10 | | strategy, tactics | 15 | | tower defense | 20 | | action (default) | 30 | | racing, fighting, twitch shooters, physics sports | 60 | Change it to your game's own pace: `gripforge_gamekit_configure { id: "game.engine", config: { tick: { hz: 20 } } }`. ## Threads on the web Rendering runs in a Worker (OffscreenCanvas) and physics in its own Worker by default, when the page is cross-origin isolated (shared memory). Otherwise they fall back to the main thread and the reason is shown. No thread waits on another: each publishes its latest state (double buffer + seq), commands go through queues. ## Engines The GripForge host steps the tick from the frame callback with the real frame time: Godot `_process`, Unity `Update`, Unreal `TickComponent`. Never run game logic in `_physics_process`, `FixedUpdate` or an async physics tick, and never change the engine's physics step (`physics_ticks_per_second`, `Time.fixedDeltaTime`): the pace of the game is `tick.hz`. Read the tick with `tick_info()` / `TickInfo()` and `tick_alpha()` / `TickAlpha()`. ## Check your game or kit ```sh gripforge game-check --loop ./my-game # the core loop rule alone, on every source and game document gripforge game-check ./my-game # the production check, core loop included gripforge kit build ./my-kit # refuses a kit that breaks the rule, before compiling ``` MCP: `gripforge_core_loop_check { path }` (local), `gripforge_game_audit` (a missing tick, fixed for free with `fix: true`). | Rule | Fails when | Fix | |---|---|---| | `tick_missing` | `game.engine` has no `tick.hz` | Set the tick of your genre | | `frame_mutates_state` | a frame / present hook or a `requestAnimationFrame` callback writes the game state | Move the write to the tick; the frame reads and interpolates | | `physics_runs_gameplay` | gameplay, or the GripForge tick, runs from a physics callback | Keep the physics callback for physics; rules run on the tick | | `engine_physics_retuned` | the engine physics step is changed | Leave it; change `tick.hz` | | `worker_disabled` | `playerWorker=0` in a game (a URL/localStorage debug option `?worker.render=0` is fine) | Remove it, or state the reason | | `shared_memory_disabled` | `memory.shared: false` | Remove it, or state the reason | | `physics_main_thread` | physics placed on `main`, or `placement: false` | Remove it, or state the reason | | `sync_worker_call` | a tick path waits (`Atomics.wait`, synchronous call, `await`, `block_on`, `.Wait()`, `.Result`) | Post a command; read the latest published state next tick | A switch you really need passes with its reason: a `core-loop-reason: …` comment on the line (or the line above), or `"coreLoopReasons": { "worker_disabled": "…" }` in the game document.