Godot Delta Contract

WorldTime is delta-agnostic: WorldTimeAdvanceSystem.Update(float deltaTime) trusts whatever you pass as real seconds for that advance. It does not read or reset Engine.TimeScale.

The contract

Term Meaning
Real second Wall-clock progression for one advance, before GameSecondsPerRealSecond
Game second realDelta × TimeScale.GameSecondsPerRealSecond
Single owner of sim speed TimeScale.GameSecondsPerRealSecond (and optional fixed-step tick count). Not Engine.TimeScale
// WorldTimeAdvanceSystem (Core)
float gameDeltaSeconds = deltaTime * state.TimeScale.GameSecondsPerRealSecond;
state.AccumulatedRealMicroseconds += RoundToMicroseconds(deltaTime);

AccumulatedRealMicroseconds sums the same deltaTime you pass. If that delta was already scaled by Godot, the accumulator is not wall-clock time.

Engine.TimeScale compounds (Trap 6)

Godot multiplies _Process(double delta) by Engine.TimeScale:

// If Engine.TimeScale = 2 and GameSecondsPerRealSecond = 60:
//   gameDelta = (realWall × 2) × 60  →  120 game-sec per wall-sec, not 60
Bootstrap.Advance((float)delta); // delta is already engine-scaled

Policy: Keep Engine.TimeScale = 1.0 for gameplay simulation. Use GameSecondsPerRealSecond, pause flags, and tick scheduling for fast-forward.

Runtime guards

Layer Behavior
TimeAdvanceDeltaGuard (Core) Rejects NaN/negative/infinite deltas; warns on steps > 10s
GodotTimeDeltaContract Pins Engine.TimeScale to 1; debug warning once if it was not 1
MoonBark SimulationTimeGuards Timer vs FixedStepSeconds check; pins engine scale each tick
MoonBark SimulationStepContract Throws if WorldTime/runner step ≠ FixedStepSeconds

Frame-driven demos can use WorldTimeTickNode; shipping games should use a fixed real step and explicit tick count (MoonBark Idle pattern):

Timer (WaitTime = 1/60)
  → AdvanceSeconds(waitTime × hudSpeedMultiplier)
    → N ticks × WorldTime.Advance(FixedStepSeconds)   // always 1/60 real sec per tick
    → systemRunner.Update(FixedStepSeconds)

Benefits:

  • Real elapsed per tick is obvious: ticks × FixedStepSeconds
  • HUD speed multiplies how many ticks, not ambiguous _Process delta
  • No interaction with Engine.TimeScale

See MoonBark Idle time contract for a full consumer example.

Frame-driven integration (demos / simple scenes)

Acceptable for demos when Engine.TimeScale stays at 1.0:

public override void _Process(double delta)
{
    _bootstrap.Advance((float)delta); // delta = real seconds this frame (if Engine.TimeScale == 1)
}

Or wire WorldTimeTickNode — same rules apply.

Pause and fast-forward

Goal Use
Pause sim WorldTimeEcsRuntime.Pause() / IsPaused
2× game calendar TimeScale.GameSecondsPerRealSecond = 120 (example)
3× tick rate Run 3 fixed steps per timer fire
Global Godot slow-mo Engine.TimeScale only if you do not also scale via WorldTime, or unscale delta before Update

WorldTime pause preserves AccumulatedRealMicroseconds exactly; engine pause does not substitute.

Unscaling delta (advanced, not default)

If you must keep Engine.TimeScale != 1 for unrelated engine effects:

float realDelta = (float)delta / (float)Engine.TimeScale;
runtime.Update(realDelta);

Document this at the call site. Prefer fixed-step + GameSecondsPerRealSecond instead.