Files
mrgameeng/docs/architecture.md
T
Leonid PershinandClaude Fable 5 e06f24a319 Parallel rendering pipeline, ring vertex buffer, phase timings
Five optimizations measured on the 100k-entity stress scene (Release,
vsync off): 103 FPS baseline -> 297 FPS.

- Sprite submission and vertex building run on all cores above
  Renderer2DOptions.ParallelThreshold (default 8192). Work is sliced
  into 4096-entity segments: a Friflo chunk holds a whole archetype,
  so per-chunk parallelism degenerates to one thread. Segments merge
  in deterministic order, preserving radix sort stability.
- Vertex buffer is ring-written with SetDataOptions.NoOverwrite
  (GPU buffer 2x frame size); Discard only on wrap-around.
- Texture2DRegion precomputes UVs - four float divisions per sprite
  per frame removed.
- Renderer2D exposes per-phase timings (submit/sort/build/upload/draw),
  shown in the sample HUD - all further optimization is data-driven.
- Sample BounceSystem parallelized the same segmented way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 05:06:55 +03:00

17 KiB
Raw Blame History

Архитектура mrgameeng

Обзор

mrgameeng — кроссплатформенный 2D игровой движок на базе MonoGame 3.8.4 (DesktopGL: Windows, Linux, macOS), .NET 8, C#.

Ключевое архитектурное решение — ECS-first: движок плотно интегрирует Friflo.Engine.ECS (v3.6) как ядро модели объектов. Это самая производительная ECS-библиотека в .NET по результатам Ecs.CSharp.Benchmark: ноль аллокаций в горячих путях, archetype-хранилище, встроенные системы, события и индексы.

Принципы

  1. Данные отдельно от логики. Всё состояние игры — в компонентах (struct : IComponent), вся логика — в системах (QuerySystem<T>). Никаких Update() у игровых объектов и иерархий наследования сущностей.
  2. Ноль аллокаций в кадре. Системы, выполняющиеся каждый кадр, не должны аллоцировать память. Проверяется бенчмарками и code review.
  3. Модульность. Движок разбит на библиотеки по функциональным областям. Игра подключает только то, что использует.
  4. Всё демонстрируется. Каждая публичная возможность движка показана в MrGameEng.Sample и покрыта тестами (где логика тестируема без GPU).

Модули

Библиотека Ответственность
MrGameEng.Core Игровой цикл (хост над Game), EntityStore, SystemRoot, сцены, время, жизненный цикл
MrGameEng.Graphics Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои
MrGameEng.Input Абстракция ввода: клавиатура, мышь, геймпад; action maps
MrGameEng.Audio Звуковые эффекты и музыка
MrGameEng.Assets Runtime-загрузка ресурсов без Content Pipeline, кэш, AssetRef<T>
MrGameEng.Assets.Generator Roslyn incremental source generator: классы с типизированными хендлами ресурсов
MrGameEng.UI Игровой UI на Myra: Desktop на сцену, виджеты, скининг

Планируемые модули (по мере развития): Physics2D, Tilemap, UI, Particles.

Правило зависимостей

MrGameEng.Graphics ─┐
MrGameEng.Input    ─┤
MrGameEng.Audio    ─┼──► MrGameEng.Core ──► MonoGame.Framework.DesktopGL
MrGameEng.Assets   ─┘                  └──► Friflo.Engine.ECS

Модули зависят только от Core и никогда друг от друга. Core зависит только от MonoGame и Friflo. Если двум модулям нужен общий тип — он переезжает в Core.

MrGameEng.Assets.Generator — особый случай: это анализатор (netstandard2.0), он подключается к проекту игры как Analyzer, в рантайме не участвует и не зависит от других модулей движка.

UI (Myra)

Игровой UI — интеграция 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/ проекта игры как сырые файлы (копируются в output при сборке) и грузятся в рантайме:

Тип Форматы Как грузим
Текстуры png, jpg Texture2D.FromFile
Шрифты ttf FontStashSharp (растеризация в рантайме, динамический атлас)
Звуки wav SoundEffect.FromFile
Музыка ogg Стриминг через NVorbis + DynamicSoundEffectInstance
Шейдеры fx Компиляция dotnet-mgfxc на этапе сборки (MSBuild target), в рантайме грузится байткод .mgfx

AssetManagerMrGameEng.Assets) грузит ресурсы по типизированному хендлу AssetRef<T> (путь + тип), кэширует по пути и владеет временем жизни (Dispose при выгрузке сцены/игры).

Кодогенератор хендлов

MrGameEng.Assets.Generator — Roslyn incremental source generator. Файлы из Assets/ передаются ему через AdditionalFiles (один glob в .csproj игры). По дереву папок он генерирует статический класс с типизированными хендлами, тип выводится из расширения файла:

// Assets/Textures/player.png  →
public static partial class GameAssets
{
    public static class Textures
    {
        public static readonly AssetRef<Texture2D> Player = new("Textures/player.png");
    }
}

// использование: Texture2D tex = assets.Load(GameAssets.Textures.Player);

Обращение к ресурсу по строковому пути в коде игры — запрещено соглашением; строки существуют только внутри сгенерированного кода.

Рендеринг

SpriteBatch в движке не используется — в MrGameEng.Graphics свой батчер, спроектированный под ECS.

Камера

Ортографическая 2D-камера — из коробки, отдельная сущность с компонентом Camera:

  • Позиция, зум, поворот, опциональные границы мира (clamp).
  • Строит ортографическую view-projection матрицу; виртуальное разрешение с letterbox/scale под размер окна.
  • Активная камера — одна на сцену. Утилиты ScreenToWorld / WorldToScreen.

Слои (render layers)

  • Слои регистрируются явно при настройке рендерера: имя + порядок отрисовки (например, Background → World → Foreground → UI).
  • У каждого слоя — пространство: World (через трансформ камеры) или Screen (screen-space, без камеры — HUD, UI).
  • Сортировка внутри слоя — по float depth спрайта; слой можно переключить в режим Y-sort (depth = позиция Y, для top-down игр).
  • Спрайт ссылается на слой компактным id (LayerId), не строкой.

Culling

  • Перед записью вершин AABB спрайта (с учётом rotation/scale) проверяется против world-прямоугольника камеры; невидимые сущности не попадают в батчер.
  • Screen-space слои не куллятся по камере (они всегда на экране).
  • Culling — простой broad-phase в draw-системе, без пространственных структур; если профилирование покажет, что на больших мирах его не хватает, добавим spatial hash отдельным этапом.

Батчер

  • Draw-системы итерируют чанки Friflo (Chunks<Position, Sprite, ...>) и пишут вершины напрямую в CPU-буфер батчера — без промежуточных списков и аллокаций.
  • Порядок сортировки: слой → depth (или Y) → текстура; спрайты с одной текстурой сливаются в один draw call (динамический vertex buffer + общий quad index buffer).
  • Сортировка — стабильный LSD radix sort: спрайты с равным ключом сохраняют порядок сабмита между кадрами (нет мерцания), сложность O(n); проходы по одинаковым у всех ключей разрядам пропускаются.
  • Горячий путь без тригонометрии, корней и делений: для спрайтов без поворота SinCos не вычисляется; радиус culling-окружности и UV-координаты предрассчитаны в Texture2DRegion.
  • Параллелизм: выше Renderer2DOptions.ParallelThreshold (по умолчанию 8192) подача спрайтов и построение вершин идут на всех ядрах. Работа режется на сегменты по 4096 сущностей (чанк Friflo держит весь архетип — сам по себе он слишком крупный для распределения); сегменты сливаются в детерминированном порядке, поэтому стабильность сортировки сохраняется. Ниже порога — прежний однопоточный путь без аллокаций.
  • Vertex buffer пишется кольцом (SetDataOptions.NoOverwrite, GPU-буфер вдвое больше кадра): загрузка не ждёт, пока GPU дорисует предыдущий кадр; Discard — только на перемотке кольца.
  • Тайминги фаз кадра (submit/sort/build/upload/draw, мс) доступны как свойства Renderer2D — выводятся в HUD стресс-сцены; оптимизации делаются только по ним.
  • Текстурные атласы — первоклассный гражданин: Sprite хранит регион атласа, спрайты одного атласа батчатся автоматически.
  • Цель по производительности: ≥100k спрайтов при 60 FPS на среднем десктопе, 0 аллокаций на кадр ниже порога параллелизма. Стресс-сцена Sample: 100k сущностей (~61k в кадре) ≈ 297 FPS в Release. Производительность измеряется только в Release — Debug-сборка медленнее в 5–6 раз (нет инлайнинга JIT).

Сцены и переходы

  • SceneManager владеет активной сценой; обычное переключение откладывается до начала следующего кадра (сцена никогда не выгружается посреди собственного кадра).
  • Scenes.Switch(scene, Transition.Fade(0.5f)) — переключение с визуальным переходом: фаза закрытия (старая сцена живёт) → своп при полном покрытии → фаза открытия. Тяжёлый OnLoad новой сцены скрыт за полностью закрытым экраном.
  • Встроенные переходы: Transition.Fade(duration, color) и Transition.Wipe(duration, color) (шторка). Свои — наследованием от Transition (рисование через TransitionRenderer.Fill в нормализованных координатах экрана).
  • Переходы идут по unscaled-времени: работают при паузе геймплея (TimeScale = 0).
  • Повторный Switch во время перехода заменяет целевую сцену, не перезапуская переход.

Интеграция ECS

  • На каждую сцену создаётся свой EntityStore (мир) и два SystemRoot: UpdateSystems (логика) и DrawSystems (рендер). Порядок систем детерминирован и задаётся явно при регистрации.
  • Модули подключаются к сцене extension-методами из OnLoad: scene.UseRenderer2D(), scene.UseInput(), scene.UseSpriteAnimation(), context.UseAssets(), context.UseAudio(). Сервисы модулей живут в EngineContext.Services (общие между сценами), системы — в сцене.
  • Базовый трансформ — один компонент Transform2D (position + rotation + scale): один массив на чанк дешевле трёх отдельных компонентов.
  • Модули поставляют готовые компоненты и системы (например, Sprite + SpriteRenderSystem из Graphics), игра добавляет свои.

Кадр (frame pipeline)

Game.Update ──► Scene.Update ──► SystemRoot (Update-фаза): input → логика игры → анимации
Game.Draw  ──► Scene.Draw   ──► SystemRoot (Draw-фаза):  камера → culling → запись вершин в батчер → flush (draw calls)

Структура репозитория

MrGameEng.sln
src/        библиотеки движка (MrGameEng.*)
samples/    MrGameEng.Sample — демо всех возможностей
tests/      xUnit-тесты, по проекту на библиотеку
docs/       документация (русский)

Зафиксированные версии

Зависимость Версия Назначение
.NET (target framework) net8.0
MonoGame.Framework.DesktopGL 3.8.4.1 Базовый фреймворк
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 Компиляция шейдеров при сборке