Core loop

Usage guide of the loop every game runs on: the three clocks (game tick, physics step, render), the tick of each genre, render and physics Workers on the web, the engines (_process / Update / TickComponent, never _physics_process / FixedUpdate for logic), and the checks that fail a game or kit (gripforge game-check --loop, gripforge_core_loop_check, kit build, gripforge_game_audit) with their fixes.

Source: module-kits:kits/game_engine/usage/core-loop/en.mdView Markdown source

Also read offline — CLI: grip-forge docs core-loopMCP tool: gripforge_docs { "topic": "core-loop" }

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 #

ClockRateWhat runs there
Game tickgame.engine tick.hz (5..120 Hz)All game logic: state, actions, timers, rules (kit update / late_update, hooks, triggers)
Physics stepits own fixed step (1/60 s)Physics only: forces, velocities, contacts
Renderthe display rateDrawing; 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 gametick.hz
cards, collection (turn-paced)10
strategy, tactics15
tower defense20
action (default)30
racing, fighting, twitch shooters, physics sports60

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 #

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).

RuleFails whenFix
tick_missinggame.engine has no tick.hzSet the tick of your genre
frame_mutates_statea frame / present hook or a requestAnimationFrame callback writes the game stateMove the write to the tick; the frame reads and interpolates
physics_runs_gameplaygameplay, or the GripForge tick, runs from a physics callbackKeep the physics callback for physics; rules run on the tick
engine_physics_retunedthe engine physics step is changedLeave it; change tick.hz
worker_disabledplayerWorker=0 in a game (a URL/localStorage debug option ?worker.render=0 is fine)Remove it, or state the reason
shared_memory_disabledmemory.shared: falseRemove it, or state the reason
physics_main_threadphysics placed on main, or placement: falseRemove it, or state the reason
sync_worker_calla 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.