Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Root object handed to scenes and systems: time, scene manager, services and graphics device.
|
||||
/// Created by <see cref="GameHost"/>; can also be created standalone for headless tests.
|
||||
/// </summary>
|
||||
public sealed class EngineContext
|
||||
{
|
||||
/// <summary>Engine time service.</summary>
|
||||
public GameClock Clock { get; } = new();
|
||||
|
||||
/// <summary>Scene manager owning the active scene.</summary>
|
||||
public SceneManager Scenes { get; }
|
||||
|
||||
/// <summary>Registry of module services (input, audio, assets, …).</summary>
|
||||
public ServiceRegistry Services { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The graphics device. Available once the host is initialized;
|
||||
/// throws when accessed in a headless context (unit tests).
|
||||
/// </summary>
|
||||
public GraphicsDevice GraphicsDevice =>
|
||||
_graphicsDevice ?? throw new InvalidOperationException("GraphicsDevice is not available (headless context).");
|
||||
|
||||
/// <summary>True when a graphics device is attached.</summary>
|
||||
public bool HasGraphicsDevice => _graphicsDevice is not null;
|
||||
|
||||
private GraphicsDevice? _graphicsDevice;
|
||||
|
||||
/// <summary>Creates a context. Games normally never create one themselves — <see cref="GameHost"/> does.</summary>
|
||||
public EngineContext()
|
||||
{
|
||||
Scenes = new SceneManager(this);
|
||||
}
|
||||
|
||||
internal void AttachGraphicsDevice(GraphicsDevice device) => _graphicsDevice = device;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Engine time service: per-frame delta, total elapsed time, time scaling and frame counter.
|
||||
/// Advanced once per frame by <see cref="GameHost"/>.
|
||||
/// </summary>
|
||||
public sealed class GameClock
|
||||
{
|
||||
/// <summary>Seconds elapsed since the previous frame, multiplied by <see cref="TimeScale"/>.</summary>
|
||||
public float DeltaTime { get; private set; }
|
||||
|
||||
/// <summary>Seconds elapsed since the previous frame, unaffected by <see cref="TimeScale"/>.</summary>
|
||||
public float UnscaledDeltaTime { get; private set; }
|
||||
|
||||
/// <summary>Total scaled time in seconds since the game started.</summary>
|
||||
public double TotalTime { get; private set; }
|
||||
|
||||
/// <summary>Total unscaled time in seconds since the game started.</summary>
|
||||
public double UnscaledTotalTime { get; private set; }
|
||||
|
||||
/// <summary>Multiplier applied to <see cref="DeltaTime"/>. 0 pauses gameplay, 1 is real time. Never negative.</summary>
|
||||
public float TimeScale
|
||||
{
|
||||
get => _timeScale;
|
||||
set => _timeScale = value < 0f ? 0f : value;
|
||||
}
|
||||
|
||||
/// <summary>Number of completed frames since the game started.</summary>
|
||||
public long FrameCount { get; private set; }
|
||||
|
||||
private float _timeScale = 1f;
|
||||
|
||||
/// <summary>Advances the clock by one frame. Called by the host; games should not call this.</summary>
|
||||
public void Advance(float unscaledDeltaSeconds)
|
||||
{
|
||||
UnscaledDeltaTime = unscaledDeltaSeconds;
|
||||
DeltaTime = unscaledDeltaSeconds * _timeScale;
|
||||
UnscaledTotalTime += unscaledDeltaSeconds;
|
||||
TotalTime += DeltaTime;
|
||||
FrameCount++;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The engine's game loop host. Wraps MonoGame's <see cref="Game"/>: owns the
|
||||
/// <see cref="EngineContext"/>, advances the <see cref="GameClock"/> and drives the
|
||||
/// active scene's update and draw phases.
|
||||
/// </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;
|
||||
|
||||
/// <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,
|
||||
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.AttachGraphicsDevice(GraphicsDevice);
|
||||
Context.Services.Add(Window);
|
||||
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);
|
||||
base.Draw(gameTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnExiting(object sender, ExitingEventArgs args)
|
||||
{
|
||||
Context.Scenes.Switch(null);
|
||||
Context.Scenes.ApplyPending();
|
||||
base.OnExiting(sender, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <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,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MonoGame.Framework.DesktopGL" />
|
||||
<PackageReference Include="Friflo.Engine.ECS" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// An ogg music file reference. Resolved by the assets module; streamed from disk by the
|
||||
/// audio module's <c>MusicPlayer</c> rather than loaded into memory.
|
||||
/// </summary>
|
||||
/// <param name="FullPath">Absolute path of the ogg file.</param>
|
||||
public sealed record MusicTrack(string FullPath);
|
||||
@@ -0,0 +1,62 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// A scene owns its ECS world (<see cref="EntityStore"/>) and two system roots:
|
||||
/// <see cref="UpdateSystems"/> for game logic and <see cref="DrawSystems"/> for rendering.
|
||||
/// Override <see cref="OnLoad"/> to create entities and register systems.
|
||||
/// </summary>
|
||||
public abstract class Scene
|
||||
{
|
||||
/// <summary>The ECS world of this scene.</summary>
|
||||
public EntityStore Store { get; } = new();
|
||||
|
||||
/// <summary>Systems executed every update tick, in registration order.</summary>
|
||||
public SystemRoot UpdateSystems { get; }
|
||||
|
||||
/// <summary>Systems executed every draw tick, in registration order.</summary>
|
||||
public SystemRoot DrawSystems { get; }
|
||||
|
||||
/// <summary>Engine context. Valid from <see cref="OnLoad"/> until <see cref="OnUnload"/>.</summary>
|
||||
public EngineContext Context => _context ?? throw new InvalidOperationException("Scene is not loaded.");
|
||||
|
||||
/// <summary>True while the scene is the active, loaded scene.</summary>
|
||||
public bool IsLoaded => _context is not null;
|
||||
|
||||
private EngineContext? _context;
|
||||
|
||||
/// <summary>Initializes the scene's ECS world and system roots.</summary>
|
||||
protected Scene()
|
||||
{
|
||||
UpdateSystems = new SystemRoot(Store, "Update");
|
||||
DrawSystems = new SystemRoot(Store, "Draw");
|
||||
}
|
||||
|
||||
/// <summary>Called once when the scene becomes active: create entities, add systems.</summary>
|
||||
protected abstract void OnLoad();
|
||||
|
||||
/// <summary>Called once when the scene is replaced or the game exits. Release scene resources here.</summary>
|
||||
protected virtual void OnUnload() { }
|
||||
|
||||
/// <summary>Runs the update phase. Called by <see cref="SceneManager"/>.</summary>
|
||||
public virtual void Update(GameClock clock) =>
|
||||
UpdateSystems.Update(new UpdateTick(clock.DeltaTime, (float)clock.TotalTime));
|
||||
|
||||
/// <summary>Runs the draw phase. Called by <see cref="SceneManager"/>.</summary>
|
||||
public virtual void Draw(GameClock clock) =>
|
||||
DrawSystems.Update(new UpdateTick(clock.DeltaTime, (float)clock.TotalTime));
|
||||
|
||||
internal void Load(EngineContext context)
|
||||
{
|
||||
_context = context;
|
||||
OnLoad();
|
||||
}
|
||||
|
||||
internal void Unload()
|
||||
{
|
||||
OnUnload();
|
||||
_context = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Owns the active <see cref="Scene"/>. Scene switches are deferred to the start of the
|
||||
/// next update so a scene is never unloaded in the middle of its own frame.
|
||||
/// </summary>
|
||||
public sealed class SceneManager
|
||||
{
|
||||
/// <summary>The active scene, or null before the first switch is applied.</summary>
|
||||
public Scene? Current { get; private set; }
|
||||
|
||||
private readonly EngineContext _context;
|
||||
private Scene? _pending;
|
||||
private bool _hasPending;
|
||||
|
||||
internal SceneManager(EngineContext context) => _context = context;
|
||||
|
||||
/// <summary>
|
||||
/// Requests a switch to <paramref name="scene"/>. The current scene is unloaded and the new
|
||||
/// one loaded at the start of the next update tick. Passing null unloads the current scene.
|
||||
/// </summary>
|
||||
public void Switch(Scene? scene)
|
||||
{
|
||||
_pending = scene;
|
||||
_hasPending = true;
|
||||
}
|
||||
|
||||
/// <summary>Applies a pending switch, then updates the active scene. Called by the host.</summary>
|
||||
public void Update(GameClock clock)
|
||||
{
|
||||
ApplyPending();
|
||||
Current?.Update(clock);
|
||||
}
|
||||
|
||||
/// <summary>Draws the active scene. Called by the host.</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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal service locator used by engine modules to expose their services
|
||||
/// (input, audio, assets, …) to scenes and systems without coupling modules to each other.
|
||||
/// </summary>
|
||||
public sealed class ServiceRegistry
|
||||
{
|
||||
private readonly Dictionary<Type, object> _services = new();
|
||||
|
||||
/// <summary>Registers a service instance under type <typeparamref name="T"/>. Throws if already registered.</summary>
|
||||
public void Add<T>(T service) where T : class
|
||||
{
|
||||
if (!_services.TryAdd(typeof(T), service))
|
||||
{
|
||||
throw new InvalidOperationException($"Service of type {typeof(T)} is already registered.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the registered service of type <typeparamref name="T"/>. Throws if missing.</summary>
|
||||
public T Get<T>() where T : class
|
||||
{
|
||||
return _services.TryGetValue(typeof(T), out var service)
|
||||
? (T)service
|
||||
: throw new InvalidOperationException($"Service of type {typeof(T)} is not registered.");
|
||||
}
|
||||
|
||||
/// <summary>Returns the registered service of type <typeparamref name="T"/> or null.</summary>
|
||||
public T? GetOrDefault<T>() where T : class
|
||||
{
|
||||
return _services.TryGetValue(typeof(T), out var service) ? (T)service : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user