Split the platform out of Core: new Host library + HeadlessHost
CI / build-test (push) Successful in 1m24s
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
79d406a9f4
commit
2ac074004a
@@ -0,0 +1,118 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.Host;
|
||||
|
||||
/// <summary>
|
||||
/// The engine's windowed game-loop host. Wraps MonoGame's <see cref="Game"/>: owns the
|
||||
/// <see cref="EngineContext"/>, advances the <see cref="GameClock"/>, drives the active
|
||||
/// scene's update and draw phases and renders scene-transition overlays. The
|
||||
/// <see cref="GraphicsDevice"/> is published as a service so graphics modules can reach it
|
||||
/// through the context. For a loop without a window or GPU see
|
||||
/// <see cref="HeadlessHost"/> in the core.
|
||||
/// </summary>
|
||||
public class GameHost : Game
|
||||
{
|
||||
/// <summary>Engine context shared with scenes and systems.</summary>
|
||||
public EngineContext Context { get; } = new();
|
||||
|
||||
/// <summary>The graphics device manager created by the host.</summary>
|
||||
public GraphicsDeviceManager Graphics { get; }
|
||||
|
||||
private readonly GameHostOptions _options;
|
||||
private readonly Scene _initialScene;
|
||||
private TransitionRenderer? _transitionRenderer;
|
||||
|
||||
/// <summary>Creates a host that starts with <paramref name="initialScene"/>.</summary>
|
||||
public GameHost(GameHostOptions options, Scene initialScene)
|
||||
{
|
||||
_options = options;
|
||||
_initialScene = initialScene;
|
||||
|
||||
Graphics = new GraphicsDeviceManager(this)
|
||||
{
|
||||
PreferredBackBufferWidth = options.Width,
|
||||
PreferredBackBufferHeight = options.Height,
|
||||
IsFullScreen = options.Fullscreen,
|
||||
// Borderless, как и обещает GameHostOptions.Fullscreen; по умолчанию MonoGame
|
||||
// делает эксклюзивное переключение видеорежима монитора.
|
||||
HardwareModeSwitch = false,
|
||||
SynchronizeWithVerticalRetrace = options.VSync,
|
||||
};
|
||||
|
||||
IsMouseVisible = true;
|
||||
IsFixedTimeStep = options.FixedTimeStep;
|
||||
if (options.FixedTimeStep)
|
||||
{
|
||||
TargetElapsedTime = TimeSpan.FromSeconds(1.0 / options.TargetFps);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Initialize()
|
||||
{
|
||||
Window.Title = _options.Title;
|
||||
Window.AllowUserResizing = _options.AllowResizing;
|
||||
Context.Services.Add(GraphicsDevice);
|
||||
Context.Services.Add(Window);
|
||||
Context.Services.Add<Game>(this);
|
||||
base.Initialize();
|
||||
Context.Scenes.Switch(_initialScene);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Update(GameTime gameTime)
|
||||
{
|
||||
Context.Clock.Advance((float)gameTime.ElapsedGameTime.TotalSeconds);
|
||||
Context.Scenes.Update(Context.Clock);
|
||||
base.Update(gameTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Draw(GameTime gameTime)
|
||||
{
|
||||
GraphicsDevice.Clear(_options.ClearColor);
|
||||
Context.Scenes.Draw(Context.Clock);
|
||||
DrawTransitionOverlay();
|
||||
base.Draw(gameTime);
|
||||
}
|
||||
|
||||
private void DrawTransitionOverlay()
|
||||
{
|
||||
if (Context.Scenes.ActiveTransition is not OverlayTransition transition)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_transitionRenderer ??= new TransitionRenderer(GraphicsDevice);
|
||||
transition.Draw(
|
||||
_transitionRenderer,
|
||||
Context.Scenes.TransitionCoverage,
|
||||
Context.Scenes.TransitionPhase
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnExiting(object sender, ExitingEventArgs args)
|
||||
{
|
||||
Context.Scenes.Switch(null);
|
||||
Context.Scenes.ApplyPending();
|
||||
base.OnExiting(sender, args);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_transitionRenderer?.Dispose();
|
||||
_transitionRenderer = null;
|
||||
// GraphicsDevice зарегистрирован как сервис, но им владеет MonoGame:
|
||||
// base.Dispose сам его освобождает, реестру трогать нельзя.
|
||||
Context.DisposeOwnedResources(this, GraphicsDevice);
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace MrGameEng.Host;
|
||||
|
||||
/// <summary>Window and loop settings for <see cref="GameHost"/>.</summary>
|
||||
public sealed class GameHostOptions
|
||||
{
|
||||
/// <summary>Window title.</summary>
|
||||
public string Title { get; set; } = "MrGameEng";
|
||||
|
||||
/// <summary>Backbuffer width in pixels.</summary>
|
||||
public int Width { get; set; } = 1280;
|
||||
|
||||
/// <summary>Backbuffer height in pixels.</summary>
|
||||
public int Height { get; set; } = 720;
|
||||
|
||||
/// <summary>Borderless fullscreen instead of a window.</summary>
|
||||
public bool Fullscreen { get; set; }
|
||||
|
||||
/// <summary>Synchronize presentation with the display's vertical retrace.</summary>
|
||||
public bool VSync { get; set; } = true;
|
||||
|
||||
/// <summary>Run updates on a fixed timestep (<see cref="TargetFps"/>) instead of as fast as possible.</summary>
|
||||
public bool FixedTimeStep { get; set; }
|
||||
|
||||
/// <summary>Target update rate when <see cref="FixedTimeStep"/> is enabled.</summary>
|
||||
public int TargetFps { get; set; } = 60;
|
||||
|
||||
/// <summary>Color the backbuffer is cleared to each frame.</summary>
|
||||
public Color ClearColor { get; set; } = Color.CornflowerBlue;
|
||||
|
||||
/// <summary>Allow the user to resize the window.</summary>
|
||||
public bool AllowResizing { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>
|
||||
/// Maps game actions (an enum) to any number of physical bindings: keys, mouse buttons or
|
||||
/// gamepad buttons. Query by action instead of device, rebind at runtime.
|
||||
/// </summary>
|
||||
/// <typeparam name="TAction">Enum (or any value) identifying the game's actions.</typeparam>
|
||||
public sealed class ActionMap<TAction>
|
||||
where TAction : notnull
|
||||
{
|
||||
private readonly InputManager _input;
|
||||
private readonly Dictionary<TAction, List<Binding>> _bindings = new();
|
||||
|
||||
private readonly record struct Binding(Keys? Key, MouseButton? Mouse, Buttons? GamePad);
|
||||
|
||||
/// <summary>Creates an action map querying <paramref name="input"/>.</summary>
|
||||
public ActionMap(InputManager input) => _input = input;
|
||||
|
||||
/// <summary>Adds a keyboard binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, Keys key) =>
|
||||
Add(action, new Binding(key, null, null));
|
||||
|
||||
/// <summary>Adds a mouse-button binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, MouseButton button) =>
|
||||
Add(action, new Binding(null, button, null));
|
||||
|
||||
/// <summary>Adds a gamepad-button binding for <paramref name="action"/>.</summary>
|
||||
public ActionMap<TAction> Bind(TAction action, Buttons button) =>
|
||||
Add(action, new Binding(null, null, button));
|
||||
|
||||
/// <summary>Removes every binding of <paramref name="action"/> (for rebinding).</summary>
|
||||
public void Unbind(TAction action) => _bindings.Remove(action);
|
||||
|
||||
/// <summary>True while any binding of the action is held down.</summary>
|
||||
public bool IsDown(TAction action) =>
|
||||
Any(
|
||||
action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyDown(k))
|
||||
|| (b.Mouse is { } m && input.IsMouseDown(m))
|
||||
|| (b.GamePad is { } g && input.IsButtonDown(g))
|
||||
);
|
||||
|
||||
/// <summary>True only on the frame any binding of the action went down.</summary>
|
||||
public bool IsPressed(TAction action) =>
|
||||
Any(
|
||||
action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyPressed(k))
|
||||
|| (b.Mouse is { } m && input.IsMousePressed(m))
|
||||
|| (b.GamePad is { } g && input.IsButtonPressed(g))
|
||||
);
|
||||
|
||||
/// <summary>True only on the frame any binding of the action went up.</summary>
|
||||
public bool IsReleased(TAction action) =>
|
||||
Any(
|
||||
action,
|
||||
static (input, b) =>
|
||||
(b.Key is { } k && input.IsKeyReleased(k))
|
||||
|| (b.Mouse is { } m && input.IsMouseReleased(m))
|
||||
|| (b.GamePad is { } g && input.IsButtonReleased(g))
|
||||
);
|
||||
|
||||
/// <summary>Composes -1/0/+1 from two digital actions (e.g. move left / move right).</summary>
|
||||
public float GetAxis(TAction negative, TAction positive) =>
|
||||
(IsDown(positive) ? 1f : 0f) - (IsDown(negative) ? 1f : 0f);
|
||||
|
||||
private ActionMap<TAction> Add(TAction action, Binding binding)
|
||||
{
|
||||
if (!_bindings.TryGetValue(action, out var list))
|
||||
{
|
||||
list = [];
|
||||
_bindings.Add(action, list);
|
||||
}
|
||||
|
||||
list.Add(binding);
|
||||
return this;
|
||||
}
|
||||
|
||||
private bool Any(TAction action, Func<InputManager, Binding, bool> predicate)
|
||||
{
|
||||
if (!_bindings.TryGetValue(action, out var list))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var binding in list)
|
||||
{
|
||||
if (predicate(_input, binding))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>Mouse buttons addressable through <see cref="InputManager"/>.</summary>
|
||||
public enum MouseButton
|
||||
{
|
||||
/// <summary>Left button.</summary>
|
||||
Left,
|
||||
|
||||
/// <summary>Right button.</summary>
|
||||
Right,
|
||||
|
||||
/// <summary>Middle button (wheel click).</summary>
|
||||
Middle,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Polls keyboard, mouse and gamepad once per frame and keeps the previous frame's state,
|
||||
/// enabling edge queries (<c>Pressed</c> = went down this frame, <c>Released</c> = went up).
|
||||
/// Registered as a service by <c>scene.UseInput()</c>; polled by <see cref="InputSystem"/>
|
||||
/// at the start of the update phase. While <see cref="MrGameEng.Core.InputCapture.Captured"/>
|
||||
/// is set (e.g. the developer console is open), game-facing input reads as released.
|
||||
/// </summary>
|
||||
public sealed class InputManager
|
||||
{
|
||||
private readonly MrGameEng.Core.InputCapture? _capture;
|
||||
|
||||
private KeyboardState _keyboard;
|
||||
private KeyboardState _previousKeyboard;
|
||||
private MouseState _mouse;
|
||||
private MouseState _previousMouse;
|
||||
private GamePadState _gamePad;
|
||||
private GamePadState _previousGamePad;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a manager. With a <paramref name="capture"/>, input is suppressed while a UI
|
||||
/// overlay holds it (keys/buttons read as released, mouse position and wheel freeze).
|
||||
/// </summary>
|
||||
public InputManager(MrGameEng.Core.InputCapture? capture = null) => _capture = capture;
|
||||
|
||||
/// <summary>Polls all devices. Called once per frame by <see cref="InputSystem"/>.</summary>
|
||||
public void Update()
|
||||
{
|
||||
if (_capture?.Captured == true)
|
||||
{
|
||||
// Оверлей (консоль) захватил ввод: клавиши и кнопки считаются отпущенными,
|
||||
// позиция мыши и счётчик колеса замораживаются — все дельты нулевые.
|
||||
Apply(
|
||||
default,
|
||||
new MouseState(
|
||||
_mouse.X,
|
||||
_mouse.Y,
|
||||
_mouse.ScrollWheelValue,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released
|
||||
),
|
||||
default
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Apply(Keyboard.GetState(), Mouse.GetState(), GamePad.GetState(PlayerIndex.One));
|
||||
}
|
||||
|
||||
internal void Apply(KeyboardState keyboard, MouseState mouse, GamePadState gamePad)
|
||||
{
|
||||
_previousKeyboard = _keyboard;
|
||||
_previousMouse = _mouse;
|
||||
_previousGamePad = _gamePad;
|
||||
_keyboard = keyboard;
|
||||
_mouse = mouse;
|
||||
_gamePad = gamePad;
|
||||
}
|
||||
|
||||
/// <summary>True while the key is held down.</summary>
|
||||
public bool IsKeyDown(Keys key) => _keyboard.IsKeyDown(key);
|
||||
|
||||
/// <summary>True only on the frame the key went down.</summary>
|
||||
public bool IsKeyPressed(Keys key) =>
|
||||
_keyboard.IsKeyDown(key) && _previousKeyboard.IsKeyUp(key);
|
||||
|
||||
/// <summary>True only on the frame the key went up.</summary>
|
||||
public bool IsKeyReleased(Keys key) =>
|
||||
_keyboard.IsKeyUp(key) && _previousKeyboard.IsKeyDown(key);
|
||||
|
||||
/// <summary>Mouse cursor position in window pixels.</summary>
|
||||
public Point MousePosition => _mouse.Position;
|
||||
|
||||
/// <summary>Cursor movement since the previous frame.</summary>
|
||||
public Point MouseDelta => _mouse.Position - _previousMouse.Position;
|
||||
|
||||
/// <summary>Scroll wheel change since the previous frame (positive = up).</summary>
|
||||
public int WheelDelta => _mouse.ScrollWheelValue - _previousMouse.ScrollWheelValue;
|
||||
|
||||
/// <summary>True while the mouse button is held down.</summary>
|
||||
public bool IsMouseDown(MouseButton button) => GetButton(_mouse, button) == ButtonState.Pressed;
|
||||
|
||||
/// <summary>True only on the frame the mouse button went down.</summary>
|
||||
public bool IsMousePressed(MouseButton button) =>
|
||||
GetButton(_mouse, button) == ButtonState.Pressed
|
||||
&& GetButton(_previousMouse, button) == ButtonState.Released;
|
||||
|
||||
/// <summary>True only on the frame the mouse button went up.</summary>
|
||||
public bool IsMouseReleased(MouseButton button) =>
|
||||
GetButton(_mouse, button) == ButtonState.Released
|
||||
&& GetButton(_previousMouse, button) == ButtonState.Pressed;
|
||||
|
||||
/// <summary>True while the gamepad button is held down.</summary>
|
||||
public bool IsButtonDown(Buttons button) => _gamePad.IsButtonDown(button);
|
||||
|
||||
/// <summary>True only on the frame the gamepad button went down.</summary>
|
||||
public bool IsButtonPressed(Buttons button) =>
|
||||
_gamePad.IsButtonDown(button) && _previousGamePad.IsButtonUp(button);
|
||||
|
||||
/// <summary>True only on the frame the gamepad button went up.</summary>
|
||||
public bool IsButtonReleased(Buttons button) =>
|
||||
_gamePad.IsButtonUp(button) && _previousGamePad.IsButtonDown(button);
|
||||
|
||||
/// <summary>Left thumbstick, x/y in [-1, 1]. Y is inverted to match the engine's y-down world.</summary>
|
||||
public Vector2 LeftStick => new(_gamePad.ThumbSticks.Left.X, -_gamePad.ThumbSticks.Left.Y);
|
||||
|
||||
private static ButtonState GetButton(in MouseState state, MouseButton button) =>
|
||||
button switch
|
||||
{
|
||||
MouseButton.Left => state.LeftButton,
|
||||
MouseButton.Right => state.RightButton,
|
||||
MouseButton.Middle => state.MiddleButton,
|
||||
_ => ButtonState.Released,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.Input;
|
||||
|
||||
/// <summary>Polls the <see cref="InputManager"/> once per frame. Registered first in the update phase.</summary>
|
||||
public sealed class InputSystem : BaseSystem
|
||||
{
|
||||
private readonly InputManager _input;
|
||||
|
||||
/// <summary>Creates the system for <paramref name="input"/>.</summary>
|
||||
public InputSystem(InputManager input) => _input = input;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnUpdateGroup() => _input.Update();
|
||||
}
|
||||
|
||||
/// <summary>Wires the input module into a <see cref="Scene"/>.</summary>
|
||||
public static class SceneInputExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the shared <see cref="InputManager"/> service (creating it on first use) and
|
||||
/// inserts <see cref="InputSystem"/> at the start of the scene's update phase.
|
||||
/// Call from <c>OnLoad</c> before adding gameplay systems.
|
||||
/// </summary>
|
||||
public static InputManager UseInput(this Scene scene)
|
||||
{
|
||||
var services = scene.Context.Services;
|
||||
var input = services.GetOrDefault<InputManager>();
|
||||
if (input is null)
|
||||
{
|
||||
var capture = services.GetOrDefault<InputCapture>();
|
||||
if (capture is null)
|
||||
{
|
||||
capture = new InputCapture();
|
||||
services.Add(capture);
|
||||
}
|
||||
|
||||
input = new InputManager(capture);
|
||||
services.Add(input);
|
||||
}
|
||||
|
||||
scene.UpdateSystems.Insert(0, new InputSystem(input));
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="MrGameEng.Host.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace MrGameEng.Host;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal overlay renderer handed to <see cref="OverlayTransition.Draw"/>: fills rectangles
|
||||
/// in normalized screen coordinates (0..1 on both axes) over the rendered scene.
|
||||
/// </summary>
|
||||
public sealed class TransitionRenderer : IDisposable
|
||||
{
|
||||
private readonly GraphicsDevice _device;
|
||||
private readonly BasicEffect _effect;
|
||||
private readonly VertexPositionColor[] _vertices = new VertexPositionColor[6];
|
||||
|
||||
/// <summary>Disposes the GPU effect. Called by <see cref="GameHost"/> on shutdown.</summary>
|
||||
public void Dispose() => _effect.Dispose();
|
||||
|
||||
internal TransitionRenderer(GraphicsDevice device)
|
||||
{
|
||||
_device = device;
|
||||
_effect = new BasicEffect(device)
|
||||
{
|
||||
VertexColorEnabled = true,
|
||||
TextureEnabled = false,
|
||||
World = Matrix.Identity,
|
||||
View = Matrix.Identity,
|
||||
Projection = Matrix.CreateOrthographicOffCenter(0f, 1f, 1f, 0f, 0f, 1f),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a rectangle given in normalized screen coordinates with <paramref name="color"/>
|
||||
/// at the given <paramref name="opacity"/> (0 = invisible, 1 = solid).
|
||||
/// </summary>
|
||||
public void Fill(float x, float y, float width, float height, Color color, float opacity = 1f)
|
||||
{
|
||||
var alpha = (byte)(Math.Clamp(opacity, 0f, 1f) * color.A);
|
||||
var premultiplied = Color.FromNonPremultiplied(color.R, color.G, color.B, alpha);
|
||||
|
||||
var topLeft = new Vector3(x, y, 0f);
|
||||
var topRight = new Vector3(x + width, y, 0f);
|
||||
var bottomLeft = new Vector3(x, y + height, 0f);
|
||||
var bottomRight = new Vector3(x + width, y + height, 0f);
|
||||
|
||||
_vertices[0] = new VertexPositionColor(topLeft, premultiplied);
|
||||
_vertices[1] = new VertexPositionColor(topRight, premultiplied);
|
||||
_vertices[2] = new VertexPositionColor(bottomLeft, premultiplied);
|
||||
_vertices[3] = new VertexPositionColor(bottomLeft, premultiplied);
|
||||
_vertices[4] = new VertexPositionColor(topRight, premultiplied);
|
||||
_vertices[5] = new VertexPositionColor(bottomRight, premultiplied);
|
||||
|
||||
_device.BlendState = BlendState.AlphaBlend;
|
||||
_device.DepthStencilState = DepthStencilState.None;
|
||||
_device.RasterizerState = RasterizerState.CullNone;
|
||||
|
||||
foreach (var pass in _effect.CurrentTechnique.Passes)
|
||||
{
|
||||
pass.Apply();
|
||||
_device.DrawUserPrimitives(PrimitiveType.TriangleList, _vertices, 0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.Host;
|
||||
|
||||
/// <summary>
|
||||
/// A scene transition that draws a full-screen overlay through a
|
||||
/// <see cref="TransitionRenderer"/>. The timing state machine lives in
|
||||
/// <see cref="SceneManager"/>; <see cref="GameHost"/> renders the overlay each draw while a
|
||||
/// transition is active. Transitions are stateless and reusable.
|
||||
/// </summary>
|
||||
public abstract class OverlayTransition : Transition
|
||||
{
|
||||
/// <summary>Creates a transition with explicit phase durations.</summary>
|
||||
protected OverlayTransition(float outDuration, float inDuration)
|
||||
: base(outDuration, inDuration) { }
|
||||
|
||||
/// <summary>
|
||||
/// Draws the overlay. <paramref name="coverage"/> is 0 (scene fully visible) to
|
||||
/// 1 (scene fully covered); <paramref name="phase"/> tells which side of the switch this is.
|
||||
/// </summary>
|
||||
public abstract void Draw(TransitionRenderer renderer, float coverage, TransitionPhase phase);
|
||||
}
|
||||
|
||||
/// <summary>Factories for the built-in visual scene transitions.</summary>
|
||||
public static class Transitions
|
||||
{
|
||||
/// <summary>Fade through a solid color (black by default). Total duration is split between out and in.</summary>
|
||||
public static Transition Fade(float duration = 0.6f, Color? color = null) =>
|
||||
new FadeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||
|
||||
/// <summary>A curtain wiping across the screen (black by default). Total duration is split between out and in.</summary>
|
||||
public static Transition Wipe(float duration = 0.6f, Color? color = null) =>
|
||||
new WipeTransition(duration / 2f, duration / 2f, color ?? Color.Black);
|
||||
|
||||
private sealed class FadeTransition(float outDuration, float inDuration, Color color)
|
||||
: OverlayTransition(outDuration, inDuration)
|
||||
{
|
||||
public override void Draw(
|
||||
TransitionRenderer renderer,
|
||||
float coverage,
|
||||
TransitionPhase phase
|
||||
) => renderer.Fill(0f, 0f, 1f, 1f, color, coverage);
|
||||
}
|
||||
|
||||
private sealed class WipeTransition(float outDuration, float inDuration, Color color)
|
||||
: OverlayTransition(outDuration, inDuration)
|
||||
{
|
||||
public override void Draw(
|
||||
TransitionRenderer renderer,
|
||||
float coverage,
|
||||
TransitionPhase phase
|
||||
)
|
||||
{
|
||||
// Out: шторка растёт слева направо; In: уезжает дальше вправо.
|
||||
if (phase == TransitionPhase.Out)
|
||||
{
|
||||
renderer.Fill(0f, 0f, coverage, 1f, color);
|
||||
}
|
||||
else
|
||||
{
|
||||
renderer.Fill(1f - coverage, 0f, coverage, 1f, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user