M4: Stage.FixedUpdate + Time's fixed-step accumulator
Both were explicitly deferred to M4 by ITime and Stage's own doc comments back when they were written: a FixedUpdate stage with no real accumulator behind it would be actively misleading, and building the accumulator with no physics system to test it against would be untested speculative machinery. engine.physics (next) is the real consumer. Time.ConsumeFixedSteps accumulates DeltaTime and hands back how many FixedDeltaTime-sized (1/50s) steps it can pay for, capped at 5 per frame so a real stall becomes visible lag instead of a catch-up burst of physics steps. Engine.Host calls it once per frame in both the headless and windowed loops, running Stage.FixedUpdate that many times before Stage.Update — gated by IPlayModeController.IsPlaying the same way Update already is, and only accumulating time while actually playing, so entering Play doesn't open with a burst of steps for however long Edit mode had been sitting idle. 3 new tests on the accumulator itself (below-one-step, whole-steps-plus- remainder, the 5-step cap under a simulated stall). Full suite: 76 tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N1qPfzq8TDCUMFMV3UwV5N
This commit is contained in:
@@ -2,15 +2,26 @@ namespace Engine.Kernel.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only to plugins (ITime); advanced by whoever owns the frame loop —
|
||||
/// Engine.Host today, headless or windowed — via <see cref="Tick"/>, which
|
||||
/// isn't on the interface. Same split as Schedule/ISchedule: the host gets
|
||||
/// the extra, plugin-facing code doesn't.
|
||||
/// Engine.Host today, headless or windowed — via <see cref="Tick"/> and
|
||||
/// <see cref="ConsumeFixedSteps"/>, neither on the interface. Same split as
|
||||
/// Schedule/ISchedule: the host gets the extra, plugin-facing code doesn't.
|
||||
/// </summary>
|
||||
public sealed class Time : ITime
|
||||
{
|
||||
// Capped so a real stall — a debugger pause, a slow disk load — can't
|
||||
// turn into a catch-up burst of hundreds of physics steps executing
|
||||
// back to back, each one still assuming a normal frame budget around
|
||||
// it. Five steps behind (0.1s at the default rate) already means
|
||||
// something else is wrong; better to visibly fall behind real time
|
||||
// than to freeze the process trying to simulate through it.
|
||||
private const int MaxStepsPerFrame = 5;
|
||||
|
||||
private float _accumulator;
|
||||
|
||||
public float DeltaTime { get; private set; }
|
||||
public double ElapsedTime { get; private set; }
|
||||
public int FrameCount { get; private set; }
|
||||
public float FixedDeltaTime { get; } = 1f / 50f;
|
||||
|
||||
public void Tick(float deltaTime)
|
||||
{
|
||||
@@ -18,4 +29,25 @@ public sealed class Time : ITime
|
||||
ElapsedTime += deltaTime;
|
||||
FrameCount++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds this frame's DeltaTime to the accumulator and returns how many
|
||||
/// FixedDeltaTime-sized steps it can pay for, having already deducted
|
||||
/// that much. Call once per frame, then run Stage.FixedUpdate that many
|
||||
/// times (zero is normal — most frames are shorter than FixedDeltaTime
|
||||
/// at 50Hz against a 60+fps display).
|
||||
/// </summary>
|
||||
public int ConsumeFixedSteps()
|
||||
{
|
||||
_accumulator += DeltaTime;
|
||||
|
||||
var steps = 0;
|
||||
while (_accumulator >= FixedDeltaTime && steps < MaxStepsPerFrame)
|
||||
{
|
||||
_accumulator -= FixedDeltaTime;
|
||||
steps++;
|
||||
}
|
||||
|
||||
return steps;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user