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 |
Recommended integration: fixed-step timer (production games)
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
_Processdelta - 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.
Related
- Time scaling consumption — traps, events, save/load
- Custom time scale —
GameSecondsPerRealSecondpresets - Time precision — int64 microseconds