Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s

This commit is contained in:
Leonid Pershin
2026-06-11 04:03:07 +03:00
parent 31aba3aeee
commit ff2231a8ab
72 changed files with 4113 additions and 0 deletions
+39
View File
@@ -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;
}
+42
View File
@@ -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++;
}
}
+77
View File
@@ -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);
}
}
+34
View File
@@ -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;
}
+12
View File
@@ -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>
+8
View File
@@ -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);
+62
View File
@@ -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;
}
}
+51
View File
@@ -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);
}
}
+33
View File
@@ -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;
}
}