Add MrGameEng.UI module integrating Myra

scene.UseUI() creates a per-scene Myra Desktop and registers
UiRenderSystem last in the draw phase (UI on top, window pixels).
GameHost now exposes itself as a Game service (Myra needs the instance;
useful for games too). Myra was picked over Gum/ImGui for the
FontStashSharp ecosystem fit, pipeline-free assets and maturity; its
internal SpriteBatch use is a documented exception to the engine rule.

Sample: on-screen HUD replaces window-title-only stats — live FPS label,
music volume slider and scene-switch buttons on both scenes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-11 04:51:12 +03:00
co-authored by Claude Fable 5
parent a3e6d3bb0a
commit af319d1276
12 changed files with 159 additions and 8 deletions
@@ -11,6 +11,7 @@
<ProjectReference Include="..\..\src\MrGameEng.Input\MrGameEng.Input.csproj" />
<ProjectReference Include="..\..\src\MrGameEng.Audio\MrGameEng.Audio.csproj" />
<ProjectReference Include="..\..\src\MrGameEng.Assets\MrGameEng.Assets.csproj" />
<ProjectReference Include="..\..\src\MrGameEng.UI\MrGameEng.UI.csproj" />
<ProjectReference Include="..\..\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
+10 -4
View File
@@ -113,8 +113,9 @@ public sealed class SceneHotkeysSystem(
}
}
/// <summary>FPS и статистика рендера в заголовке окна (обновляется 4 раза в секунду).</summary>
public sealed class TitleStatsSystem(EngineContext context, Renderer2D renderer, string sceneName) : BaseSystem
/// <summary>FPS и статистика рендера: в HUD-лейбл и в заголовок окна (4 раза в секунду).</summary>
public sealed class StatsSystem(
EngineContext context, Renderer2D renderer, string sceneName, Myra.Graphics2D.UI.Label? hudLabel = null) : BaseSystem
{
private float _accumulated;
private int _frames;
@@ -131,8 +132,13 @@ public sealed class TitleStatsSystem(EngineContext context, Renderer2D renderer,
var fps = _frames / _accumulated;
_accumulated = 0f;
_frames = 0;
context.Services.Get<GameWindow>().Title =
$"MrGameEng Sample — {sceneName} | {fps:F0} FPS | sprites: {renderer.SubmittedSprites} | culled: {renderer.CulledSprites} | draw calls: {renderer.DrawCalls}";
var stats =
$"{fps:F0} FPS | sprites: {renderer.SubmittedSprites} | culled: {renderer.CulledSprites} | draw calls: {renderer.DrawCalls}";
context.Services.Get<GameWindow>().Title = $"MrGameEng Sample — {sceneName} | {stats}";
if (hudLabel is not null)
{
hudLabel.Text = $"{sceneName}\n{stats}";
}
}
}
+24 -1
View File
@@ -5,6 +5,8 @@ using MrGameEng.Audio;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Input;
using MrGameEng.UI;
using Myra.Graphics2D.UI;
namespace MrGameEng.Sample.Scenes;
@@ -90,11 +92,32 @@ public sealed class MainScene : Scene
var hud = new Sprite(shapeRegions[3], SampleLayers.Ui);
Store.CreateEntity(new Transform2D(new Vector2(16f, 16f)), hud);
// UI (Myra): HUD со статистикой, слайдер громкости, кнопка перехода. После UseRenderer2D!
var desktop = this.UseUI();
var statsLabel = new Label();
var volumeSlider = new HorizontalSlider { Minimum = 0f, Maximum = 1f, Value = audio.Music.Volume, Width = 180 };
volumeSlider.ValueChanged += (_, _) => audio.Music.Volume = volumeSlider.Value;
var switchButton = new Button { Content = new Label { Text = "Stress scene (Tab)" } };
switchButton.Click += (_, _) =>
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new StressScene(), Transition.Fade(0.8f));
}
};
var panel = new VerticalStackPanel { Left = 12, Top = 12, Spacing = 6 };
panel.Widgets.Add(statsLabel);
panel.Widgets.Add(new Label { Text = "Music volume (M — pause)" });
panel.Widgets.Add(volumeSlider);
panel.Widgets.Add(switchButton);
desktop.Root = panel;
UpdateSystems.Add(new PlayerControlSystem(player, actions, audio, beep));
UpdateSystems.Add(new BounceSystem(WorldBounds));
UpdateSystems.Add(new CameraControlSystem(camera, player, input));
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new StressScene(), Transition.Fade(0.8f)));
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, "Main"));
UpdateSystems.Add(new StatsSystem(Context, renderer, "Main", statsLabel));
var music = audio.Music;
if (!music.IsPlaying)
+20 -1
View File
@@ -4,6 +4,7 @@ using MrGameEng.Assets;
using MrGameEng.Core;
using MrGameEng.Graphics;
using MrGameEng.Input;
using MrGameEng.UI;
namespace MrGameEng.Sample.Scenes;
@@ -52,10 +53,28 @@ public sealed class StressScene : Scene
var camera = Store.CreateEntity(new Camera(Vector2.Zero, zoom: 0.3f));
var desktop = this.UseUI();
var statsLabel = new Myra.Graphics2D.UI.Label();
var backButton = new Myra.Graphics2D.UI.Button
{
Content = new Myra.Graphics2D.UI.Label { Text = "Back to main (Tab)" },
};
backButton.Click += (_, _) =>
{
if (!Context.Scenes.IsTransitioning)
{
Context.Scenes.Switch(new MainScene(), Transition.Wipe(0.8f, Color.DarkSlateBlue));
}
};
var panel = new Myra.Graphics2D.UI.VerticalStackPanel { Left = 12, Top = 12, Spacing = 6 };
panel.Widgets.Add(statsLabel);
panel.Widgets.Add(backButton);
desktop.Root = panel;
UpdateSystems.Add(new BounceSystem(WorldBounds));
UpdateSystems.Add(new StressCameraSystem(camera, input));
UpdateSystems.Add(new SceneHotkeysSystem(Context, actions, () => new MainScene(), Transition.Wipe(0.8f, Color.DarkSlateBlue)));
UpdateSystems.Add(new TitleStatsSystem(Context, renderer, $"Stress {SpriteCount:N0}"));
UpdateSystems.Add(new StatsSystem(Context, renderer, $"Stress {SpriteCount:N0}", statsLabel));
}
/// <summary>Только зум колесом — чтобы регулировать число видимых спрайтов.</summary>