File size: 2,161 Bytes
3ce1a40 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | namespace Repoverse.Core.Simulation;
/// <summary>
/// Fixed-step simulation time, decoupled from render frames (§31). The host calls
/// <see cref="Advance"/> with real frame time; the clock runs zero or more fixed ticks
/// scaled by <see cref="Speed"/>, and fires scheduled actions in tick order.
/// </summary>
public sealed class SimulationClock(double ticksPerSecond = WorldConstants.SimulationTicksPerSecond, int maxTicksPerAdvance = 16)
{
private readonly PriorityQueue<(long Tick, long Seq, Action Action), (long, long)> _scheduled = new();
private double _accumulator;
private long _seq;
public long Tick { get; private set; }
public double Speed { get; set; } = 1.0;
public bool Paused { get; set; }
public TimeSpan TickLength => TimeSpan.FromSeconds(1 / ticksPerSecond);
public TimeSpan SimulatedTime => TickLength * Tick;
/// <summary>Raised once per fixed tick, after that tick's scheduled actions.</summary>
public event Action<long>? Ticked;
public void Schedule(long ticksFromNow, Action action)
{
ArgumentOutOfRangeException.ThrowIfNegative(ticksFromNow);
var at = Tick + ticksFromNow;
_scheduled.Enqueue((at, _seq, action), (at, _seq++));
}
/// <returns>Number of ticks run.</returns>
public int Advance(TimeSpan realElapsed)
{
if (Paused || Speed <= 0) return 0;
_accumulator += realElapsed.TotalSeconds * Speed * ticksPerSecond;
int ran = 0;
while (_accumulator >= 1 && ran < maxTicksPerAdvance)
{
_accumulator -= 1;
Step();
ran++;
}
// Spiral-of-death guard: drop backlog rather than stall the frame.
if (ran == maxTicksPerAdvance) _accumulator = Math.Min(_accumulator, 1);
return ran;
}
/// <summary>Runs exactly one tick regardless of pause; used for deterministic stepping.</summary>
public void Step()
{
Tick++;
while (_scheduled.TryPeek(out var item, out _) && item.Tick <= Tick)
{
_scheduled.Dequeue();
item.Action();
}
Ticked?.Invoke(Tick);
}
}
|