namespace MrGameEng.Core;
///
/// Discrete game-speed control layered over : a pause plus an
/// ordered list of speed multipliers (1×, 3×, 6× by default). Pausing remembers the current
/// running step so restores it. fires on every
/// transition so UI (speed buttons, indicators) can refresh. Deterministic and GPU-free;
/// registered as a service via .
///
public sealed class GameSpeed
{
private readonly GameClock _clock;
private readonly float[] _steps;
private int _stepIndex;
private bool _paused;
///
/// Creates a controller that writes to
/// . are the running multipliers in
/// ascending order; each must be positive. Empty defaults to 1×, 3×, 6×.
///
public GameSpeed(GameClock clock, params float[] steps)
{
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_steps = steps is { Length: > 0 } ? (float[])steps.Clone() : [1f, 3f, 6f];
foreach (var step in _steps)
{
if (step <= 0f)
{
throw new ArgumentOutOfRangeException(
nameof(steps),
"Speed steps must be positive."
);
}
}
Apply();
}
/// The ordered running speeds (excludes the pause state).
public IReadOnlyList Steps => _steps;
/// Index of the active running step within .
public int StepIndex => _stepIndex;
/// True while gameplay is paused (clock time scale is 0).
public bool IsPaused => _paused;
/// Active multiplier: 0 while paused, otherwise Steps[StepIndex].
public float CurrentSpeed => _paused ? 0f : _steps[_stepIndex];
/// Raised after any change to the pause state or the active step.
public event Action? Changed;
/// Pauses gameplay, remembering the current step for .
public void Pause()
{
if (_paused)
{
return;
}
_paused = true;
Apply();
}
/// Resumes gameplay at the remembered step.
public void Resume()
{
if (!_paused)
{
return;
}
_paused = false;
Apply();
}
/// Toggles between paused and running.
public void TogglePause()
{
_paused = !_paused;
Apply();
}
/// Selects a running step by index (clamped to the valid range) and unpauses.
public void SetStep(int index)
{
_stepIndex = Math.Clamp(index, 0, _steps.Length - 1);
_paused = false;
Apply();
}
/// Steps to the next faster speed (clamped to the fastest) and unpauses.
public void Faster() => SetStep(_stepIndex + 1);
/// Steps to the next slower speed (clamped to the slowest) and unpauses.
public void Slower() => SetStep(_stepIndex - 1);
///
/// Cycles through states: pause → slowest step → … → fastest step → pause. Handy for a
/// single "next speed" key or button.
///
public void Cycle()
{
if (_paused)
{
_paused = false;
_stepIndex = 0;
}
else if (_stepIndex + 1 < _steps.Length)
{
_stepIndex++;
}
else
{
_paused = true;
}
Apply();
}
private void Apply()
{
_clock.TimeScale = CurrentSpeed;
Changed?.Invoke();
}
}
/// Wires the game-speed controller into the engine.
public static class GameSpeedEngineExtensions
{
///
/// Creates a bound to the context's clock and registers it as a
/// service. Call once at startup. are the running multipliers
/// (defaults to 1×, 3×, 6× when empty).
///
public static GameSpeed UseGameSpeed(this EngineContext context, params float[] steps)
{
var speed = new GameSpeed(context.Clock, steps);
context.Services.Add(speed);
return speed;
}
}