Files
mrgameeng/src/MrGameEng.Core/SceneManager.cs
T
Leonid PershinandClaude Fable 5 2ac074004a
CI / build-test (push) Successful in 1m24s
Split the platform out of Core: new Host library + HeadlessHost
Core now depends only on Friflo.Engine.ECS — no MonoGame, no platform.
The windowed MonoGame host moves to the new MrGameEng.Host library:
GameHost, GameHostOptions, visual scene transitions (OverlayTransition,
Transitions.Fade/Wipe, TransitionRenderer) and the Input feature
(namespace MrGameEng.Input is unchanged). Transition timing stays in
Core (SceneManager exposes ActiveTransition/TransitionCoverage/
TransitionPhase; the host draws the overlay). EngineContext loses its
GraphicsDevice property: hosts publish the device as a service and
graphics code reads it via context.GetGraphicsDevice() in Graphics.

Core gains HeadlessHost: a fixed-timestep loop without a window or GPU
(Tick/RunTicks, Run with wall-clock pacing and lag resync) for dedicated
servers, batch simulation and tests. Graphics and Audio now carry their
own MonoGame.Framework.DesktopGL reference instead of inheriting it
from Core.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:04:34 +03:00

143 lines
4.8 KiB
C#

namespace MrGameEng.Core;
/// <summary>
/// Owns the active <see cref="Scene"/>. 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
/// <see cref="Transition"/> first cover the old scene, swap at full coverage (hiding even a
/// slow <c>OnLoad</c>), then reveal the new one. Transition time is unscaled, so it works
/// while gameplay is paused.
/// </summary>
public sealed class SceneManager
{
private enum State
{
Idle,
CoveringOut,
RevealingIn,
}
/// <summary>The active scene, or null before the first switch is applied.</summary>
public Scene? Current { get; private set; }
/// <summary>True while a transition is covering or revealing.</summary>
public bool IsTransitioning => _state != State.Idle;
/// <summary>The transition currently covering or revealing, or null while idle.
/// Hosts that render overlays read this together with <see cref="TransitionCoverage"/>
/// and <see cref="TransitionPhase"/> during their draw phase.</summary>
public Transition? ActiveTransition => _state == State.Idle ? null : _transition;
/// <summary>Coverage of the active transition: 0 = scene fully visible, 1 = fully covered.</summary>
public float TransitionCoverage => Math.Clamp(_coverage, 0f, 1f);
/// <summary>Phase of the active transition. Meaningful only while <see cref="IsTransitioning"/>.</summary>
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;
/// <summary>
/// Requests a switch to <paramref name="scene"/>. 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.
/// </summary>
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;
}
}
/// <summary>Advances a transition and updates the active scene. Called by the host.</summary>
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);
}
/// <summary>Draws the active scene. Transition overlays are rendered by the host on top,
/// from <see cref="ActiveTransition"/> and <see cref="TransitionCoverage"/>.</summary>
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 ?? "<none>"}");
}
private static float Advance(
float coverage,
float direction,
float duration,
float deltaTime
) =>
duration <= 0f
? coverage + direction
: Math.Clamp(coverage + direction * deltaTime / duration, 0f, 1f);
}