CI / build-test (push) Successful in 1m24s
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>
143 lines
4.8 KiB
C#
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);
|
|
}
|