本文へ移動
cccskills
無料GitHub で公開

game-loop-and-time

Use when writing anything that moves, animates, counts down or simulates - per-frame vs fixed-step callbacks per engine, the accumulator loop with interpolation, delta-scaled smoothing, pause and time scale, and determinism.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md6.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Game Loop and Time

Overview

Two clocks run in every game: the render frame (variable, as fast as the machine allows) and the simulation step (fixed, so physics and rules behave the same everywhere). Most "it moves faster on my machine", tunnelling, jitter and desync bugs come from putting work on the wrong clock or forgetting to scale by the right delta.

Core principle: Simulate on a fixed step, render with interpolation, and scale every time-based value by the delta of the clock it runs on.

Where each clock lives

EnginePer frame (variable)Fixed stepDefault fixed rate
UnityUpdate, LateUpdate — Time.deltaTimeFixedUpdate — Time.fixedDeltaTime0.02 s (50 Hz)
Godot_process(delta)_physics_process(delta)60 ticks/s (physics/common/physics_ticks_per_second)
UnrealTick(DeltaSeconds)physics substepping / async physics tickproject physics settings
BevyUpdate — Res<Time>FixedUpdate — Res<Time> is Time<Fixed> there64 Hz
Phaserupdate(time, delta) (ms)Arcade physics' own step (fps, fixedStep)60
PixiJS 8ticker.add(t => …) — t.deltaMSyour accumulator—
three.js / Babylon.jssetAnimationLoop + THREE.Timer / engine.getDeltaTime() (ms)your accumulator, or the physics plugin's—
MonoGameUpdate(GameTime)IsFixedTimeStep = true (default)1/60 s
pygamedt = clock.tick(60) / 1000your accumulator—

Use the engine's fixed callback for physics and authoritative rules. Write your own loop only where the engine has none (web renderers, pygame, custom servers).

The accumulator loop (web, pygame, servers)

const STEP = 1 / 60;
const MAX_FRAME = 0.25;           // clamp: a 3 s tab-switch must not run 180 steps
let acc = 0;
let last = performance.now();

function frame(now: number) {
  const frameTime = Math.min((now - last) / 1000, MAX_FRAME);
  last = now;
  acc += frameTime;
  while (acc >= STEP) {
    previous.copyFrom(current);   // keep the last state for interpolation
    simulate(current, STEP);      // rules + physics, fixed dt only
    acc -= STEP;
  }
  render(interpolate(previous, current, acc / STEP));
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
  • Clamp the frame time, or one long frame triggers the "spiral of death" (more steps → longer frame → more steps).
  • Render the blend of the last two simulation states; without it, a 60 Hz simulation on a 144 Hz display stutters.
  • Engines with a fixed callback offer interpolation too: Unity Rigidbody.interpolation = Interpolate, Godot physics/common/physics_interpolation (2D since 4.3, 3D since 4.4), Bevy interpolation crates or a manual Time<Fixed>::overstep_fraction() blend.

Scale by delta — including smoothing

// ❌ frame-rate dependent: twice as fast at 120 FPS, crawls at 30
transform.position += velocity;
camera.position = Vector3.Lerp(camera.position, target, 0.1f);

// ✅ delta-scaled; exponential smoothing that reads the same at any frame rate
transform.position += velocity * Time.deltaTime;
float t = 1f - Mathf.Exp(-sharpness * Time.deltaTime);
camera.position = Vector3.Lerp(camera.position, target, t);
# ❌ counts frames: a 1 s cooldown is 0.42 s at 144 FPS
var frames_left := 60
func _process(_delta: float) -> void:
    frames_left -= 1

# ✅ counts seconds
var cooldown_left := 1.0
func _process(delta: float) -> void:
    cooldown_left = maxf(0.0, cooldown_left - delta)

Acceleration needs care too: velocity += accel * dt; position += velocity * dt (semi-implicit Euler) is stable at a fixed step and drifts at a variable one — another reason to simulate on the fixed clock.

Pause, slow motion and UI time

  • Time scale slows the game, not the menus: Unity Time.timeScale with UI on Time.unscaledDeltaTime; Godot Engine.time_scale with pause menus on process_mode = PROCESS_MODE_ALWAYS; Bevy Time<Virtual> (pausable, scalable) vs Time<Real>; Unreal SetGlobalTimeDilation with UI ticking while paused.
  • timeScale = 0 stops FixedUpdate in Unity — anything that must run while paused (menus, input polling for "unpause") cannot live there.
  • Pause on focus loss (visibilitychange on the web, OnApplicationPause, NOTIFICATION_APPLICATION_FOCUS_OUT), and clamp the first delta after resuming.

Determinism, when the design needs it

Replays, lockstep RTS and rollback netcode need the same inputs to produce the same state on every machine:

  • Fixed step only; no variable dt reaches the simulation.
  • One seeded RNG owned by the simulation; cosmetic effects use a separate generator so they cannot shift the sequence.
  • No iteration over hash maps or sets whose order is unspecified (use sorted keys, IndexMap, or arrays); no wall-clock reads inside a step.
  • Floating point differs across compilers, CPUs and WASM vs native — use fixed-point math or keep all peers on one build and platform.
  • Test it: same seed + same input log → same state hash after N steps.

Testing time-based code

it("covers the same distance at 30 and 60 steps per second", () => {
  const a = new Mover({ speed: 5 }); for (let i = 0; i < 60; i++) a.step(1 / 60);
  const b = new Mover({ speed: 5 }); for (let i = 0; i < 30; i++) b.step(1 / 30);
  expect(a.x).toBeCloseTo(5, 5);
  expect(b.x).toBeCloseTo(a.x, 5);
});

Inject dt; never sleep in a test. Engine tests advance frames explicitly (yield return new WaitForFixedUpdate(), runner.simulate_frames(n), app.update() with a manual time strategy).

Common Mistakes

  • Moving a rigidbody by writing its transform in Update/_process — it fights the physics step and tunnels.
  • Reading input in FixedUpdate: a key pressed between two steps is missed. Read in the frame callback, consume in the fixed one.
  • Delta in milliseconds treated as seconds (Phaser, PixiJS deltaMS, Babylon getDeltaTime()).
  • PixiJS ticker.deltaTime is a frame-scaled factor (≈1 at 60 FPS), not seconds.
  • A constant lerp factor for camera smoothing.

Red Flags

  • frameCount, % 60 or a frame counter used as a timer.
  • Time.time / performance.now() inside a rule class.
  • A physics fix done by raising the frame cap or relying on vsync.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

Use when deciding whether a request needs an analiz task before implementation - the conditions that require the architect's analysis versus going straight to implementation

日本語の概要は準備中です。原文の説明を表示しています。

makifbaysal/tasktrooper1122026年10月10日 更新

makifbaysal のスキルをすべて見る

このスキルの問題を報告する