namespace MrGameEng.Core; /// /// Owns the active . Plain switches are deferred to the start of the next /// update so a scene is never unloaded in the middle of its own frame. Switches with a /// first cover the old scene, swap at full coverage (hiding even a /// slow OnLoad), then reveal the new one. Transition time is unscaled, so it works /// while gameplay is paused. /// public sealed class SceneManager { private enum State { Idle, CoveringOut, RevealingIn, } /// The active scene, or null before the first switch is applied. public Scene? Current { get; private set; } /// True while a transition is covering or revealing. public bool IsTransitioning => _state != State.Idle; /// The transition currently covering or revealing, or null while idle. /// Hosts that render overlays read this together with /// and during their draw phase. public Transition? ActiveTransition => _state == State.Idle ? null : _transition; /// Coverage of the active transition: 0 = scene fully visible, 1 = fully covered. public float TransitionCoverage => Math.Clamp(_coverage, 0f, 1f); /// Phase of the active transition. Meaningful only while . public TransitionPhase TransitionPhase => _state == State.CoveringOut ? TransitionPhase.Out : TransitionPhase.In; private readonly EngineContext _context; private Scene? _pending; private bool _hasPending; private Transition? _transition; private State _state; private float _coverage; internal SceneManager(EngineContext context) => _context = context; /// /// Requests a switch to . Without a transition the swap happens at /// the start of the next update tick; with one, the old scene is first covered. /// Passing null unloads the current scene. Calling during an active transition replaces /// the pending target scene; a switch requested while a transition is revealing starts /// covering again from the current coverage. /// public void Switch(Scene? scene, Transition? transition = null) { _pending = scene; _hasPending = true; if (transition is not null && _state != State.CoveringOut) { _transition = transition; if (_state == State.Idle) { _coverage = 0f; } // Из RevealingIn закрытие продолжается с текущего coverage — без скачка. _state = State.CoveringOut; } } /// Advances a transition and updates the active scene. Called by the host. public void Update(GameClock clock) { switch (_state) { case State.Idle: ApplyPending(); break; case State.CoveringOut: _coverage = Advance( _coverage, +1f, _transition!.OutDuration, clock.UnscaledDeltaTime ); if (_coverage >= 1f) { ApplyPending(); _state = State.RevealingIn; } break; case State.RevealingIn: _coverage = Advance( _coverage, -1f, _transition!.InDuration, clock.UnscaledDeltaTime ); if (_coverage <= 0f) { _state = State.Idle; _transition = null; } break; } Current?.Update(clock); } /// Draws the active scene. Transition overlays are rendered by the host on top, /// from and . public void Draw(GameClock clock) => Current?.Draw(clock); internal void ApplyPending() { if (!_hasPending) { return; } _hasPending = false; Current?.Unload(); Current = _pending; _pending = null; Current?.Load(_context); Log.Info($"Scene switched to {Current?.GetType().Name ?? ""}"); } private static float Advance( float coverage, float direction, float duration, float deltaTime ) => duration <= 0f ? coverage + direction : Math.Clamp(coverage + direction * deltaTime / duration, 0f, 1f); }