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:
co-authored by
Claude Fable 5
parent
a3e6d3bb0a
commit
af319d1276
@@ -18,9 +18,11 @@ docs/ architecture, conventions, roadmap (Russian)
|
||||
|
||||
Engine modules: `Core` (game loop, ECS world, scenes, time), `Graphics` (custom batched
|
||||
renderer, camera, sprites), `Input`, `Audio`, `Assets` (runtime loading, no content
|
||||
pipeline), `Assets.Generator` (Roslyn source generator for typed asset handles).
|
||||
pipeline), `Assets.Generator` (Roslyn source generator for typed asset handles),
|
||||
`UI` (Myra integration: `scene.UseUI()` after `UseRenderer2D()`).
|
||||
Dependency rule: every module may depend only on `Core`; `Core` depends only on
|
||||
MonoGame and Friflo.Engine.ECS. `Assets.Generator` is a netstandard2.0 analyzer.
|
||||
Documented exception: Myra renders with its own SpriteBatch internally.
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<PackageVersion Include="Friflo.Engine.ECS" Version="3.6.0" />
|
||||
<PackageVersion Include="FontStashSharp.MonoGame" Version="1.5.6" />
|
||||
<PackageVersion Include="NVorbis" Version="0.10.5" />
|
||||
<PackageVersion Include="Myra" Version="1.6.1" />
|
||||
|
||||
<!-- Source generator -->
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
|
||||
@@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Audio", "src\MrGa
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.Input.Tests", "tests\MrGameEng.Input.Tests\MrGameEng.Input.Tests.csproj", "{0E0710AB-6132-4E64-9AFC-03B0601F92C6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MrGameEng.UI", "src\MrGameEng.UI\MrGameEng.UI.csproj", "{17EB97D5-DCF8-47DF-B810-DA45AE314170}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -173,6 +175,18 @@ Global
|
||||
{0E0710AB-6132-4E64-9AFC-03B0601F92C6}.Release|x64.Build.0 = Release|Any CPU
|
||||
{0E0710AB-6132-4E64-9AFC-03B0601F92C6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{0E0710AB-6132-4E64-9AFC-03B0601F92C6}.Release|x86.Build.0 = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|x64.Build.0 = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -189,5 +203,6 @@ Global
|
||||
{9F4D0E2A-1FAB-4DEB-AE7F-768EFB660AFD} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{D55753A2-4CC0-4BCC-80D5-01E919FF97BE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{0E0710AB-6132-4E64-9AFC-03B0601F92C6} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
|
||||
{17EB97D5-DCF8-47DF-B810-DA45AE314170} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
| `MrGameEng.Audio` | Звуковые эффекты и музыка |
|
||||
| `MrGameEng.Assets` | Runtime-загрузка ресурсов без Content Pipeline, кэш, `AssetRef<T>` |
|
||||
| `MrGameEng.Assets.Generator` | Roslyn incremental source generator: классы с типизированными хендлами ресурсов |
|
||||
| `MrGameEng.UI` | Игровой UI на [Myra](https://github.com/rds1983/Myra): `Desktop` на сцену, виджеты, скининг |
|
||||
|
||||
Планируемые модули (по мере развития): `Physics2D`, `Tilemap`, `UI`, `Particles`.
|
||||
|
||||
@@ -52,6 +53,21 @@ MrGameEng.Assets ─┘ └──► Friflo.Engine.ECS
|
||||
он подключается к проекту игры как `Analyzer`, в рантайме не участвует и не зависит
|
||||
от других модулей движка.
|
||||
|
||||
## UI (Myra)
|
||||
|
||||
Игровой UI — интеграция [Myra](https://github.com/rds1983/Myra) (выбор: экосистема
|
||||
FontStashSharp, не требует Content Pipeline, зрелая и поддерживаемая):
|
||||
|
||||
- `scene.UseUI()` в `OnLoad` **после** `UseRenderer2D()` — создаёт Myra `Desktop`
|
||||
(свой на сцену) и регистрирует `UiRenderSystem` последней в Draw-фазе.
|
||||
- UI строится кодом через `desktop.Root`; ввод (мышь/клавиатура) Myra обрабатывает
|
||||
сама во время рендера, в оконных пикселях.
|
||||
- Текст рисуется встроенным шрифтом Myra; свои шрифты — `FontSystem` через `AssetManager`.
|
||||
- **Документированное исключение из правил**: Myra рисует собственным SpriteBatch
|
||||
(запрет на SpriteBatch относится к коду движка, не к сторонним библиотекам).
|
||||
Если UI станет узким местом, её рендер можно перевести на наш батчер
|
||||
через `IMyraRenderer` (бэклог).
|
||||
|
||||
## Загрузка ресурсов (без Content Pipeline)
|
||||
|
||||
MGCB / Content Pipeline **не используется**. Все ресурсы лежат в папке `Assets/`
|
||||
@@ -198,4 +214,5 @@ docs/ документация (русский)
|
||||
| Friflo.Engine.ECS | 3.6.0 | ECS |
|
||||
| FontStashSharp.MonoGame | 1.5.6 | Шрифты (ttf) в рантайме |
|
||||
| NVorbis | 0.10.5 | Декодирование ogg |
|
||||
| Myra | 1.6.1 | Игровой UI |
|
||||
| dotnet-mgfxc (dotnet tool) | 3.8.4.1 | Компиляция шейдеров при сборке |
|
||||
|
||||
+3
-1
@@ -18,7 +18,9 @@
|
||||
- Physics2D (выбор библиотеки: Aether.Physics2D / своя)
|
||||
- Tilemap (поддержка Tiled)
|
||||
- Particles
|
||||
- UI
|
||||
- UI: загрузка MML-разметки Myra через AssetManager + хендлы в кодогенераторе
|
||||
- UI: рендер Myra через наш батчер (`IMyraRenderer`), если UI станет узким местом по draw call'ам
|
||||
- DevTools-модуль на ImGui.NET: инспектор сущностей, дебаг-панели
|
||||
- Бенчмарки BenchmarkDotNet для систем (сейчас производительность контролируется стресс-сценой)
|
||||
- Spatial hash для culling на очень больших мирах (если профилирование покажет необходимость)
|
||||
- Параллельная запись вершин (Parallel.For по чанкам), если упрёмся в CPU на ещё больших сценах
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -47,6 +47,7 @@ public class GameHost : Game
|
||||
Window.AllowUserResizing = _options.AllowResizing;
|
||||
Context.AttachGraphicsDevice(GraphicsDevice);
|
||||
Context.Services.Add(Window);
|
||||
Context.Services.Add<Game>(this);
|
||||
base.Initialize();
|
||||
Context.Scenes.Switch(_initialScene);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Myra" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,49 @@
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Myra;
|
||||
using Myra.Graphics2D.UI;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Draw system rendering the scene's Myra <see cref="Desktop"/>. Must run after the scene's
|
||||
/// world rendering (register UI last in the draw phase) — the UI is drawn on top in window
|
||||
/// pixels and also processes mouse/keyboard interaction during render.
|
||||
/// </summary>
|
||||
public sealed class UiRenderSystem : BaseSystem
|
||||
{
|
||||
private readonly Desktop _desktop;
|
||||
|
||||
/// <summary>Creates the system for <paramref name="desktop"/>.</summary>
|
||||
public UiRenderSystem(Desktop desktop) => _desktop = desktop;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnUpdateGroup() => _desktop.Render();
|
||||
}
|
||||
|
||||
/// <summary>Wires the UI module (Myra) into a <see cref="Scene"/>.</summary>
|
||||
public static class SceneUiExtensions
|
||||
{
|
||||
private static bool _environmentInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Myra <see cref="Desktop"/> for this scene and registers
|
||||
/// <see cref="UiRenderSystem"/> in the draw phase. Call from <c>OnLoad</c>
|
||||
/// <b>after</b> <c>UseRenderer2D()</c> so the UI draws on top of the world.
|
||||
/// Build the UI by assigning <see cref="Desktop.Root"/>.
|
||||
/// </summary>
|
||||
public static Desktop UseUI(this Scene scene)
|
||||
{
|
||||
// Геттер MyraEnvironment.Game бросает исключение, пока Game не задан — проверять через ??= нельзя.
|
||||
if (!_environmentInitialized)
|
||||
{
|
||||
MyraEnvironment.Game = scene.Context.Services.Get<Game>();
|
||||
_environmentInitialized = true;
|
||||
}
|
||||
|
||||
var desktop = new Desktop();
|
||||
scene.DrawSystems.Add(new UiRenderSystem(desktop));
|
||||
return desktop;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user