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; } }