Files
mrgameeng/docs/architecture.md
T
Leonid PershinandClaude Fable 5 a3e6d3bb0a Stable radix sprite sort and render hot-path optimizations
SpriteBatcher now sorts with a stable LSD radix sort: equal-key sprites
keep submission order across frames (no flicker) and passes over digits
identical in all keys are skipped, making the common single-layer case
nearly free. Hot paths avoid per-sprite trig and square roots: SinCos is
skipped for unrotated sprites and the culling radius comes from the
region's precomputed diagonal.

Stress scene (100k entities, ~61k on screen, Release): 103 -> 124 FPS.
Sample now runs with VSync off to show real frame rates.

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

14 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: классы с типизированными хендлами ресурсов

Планируемые модули (по мере развития): 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, в рантайме не участвует и не зависит от других модулей движка.

Загрузка ресурсов (без 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-окружности берётся из предрассчитанной диагонали региона.
  • Текстурные атласы — первоклассный гражданин: Sprite хранит регион атласа, спрайты одного атласа батчатся автоматически.
  • Цель по производительности: ≥100k спрайтов при 60 FPS на среднем десктопе, 0 аллокаций на кадр. Контролируется стресс-сценой в Sample: 100k сущностей (~61k в кадре) ≈ 124 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
dotnet-mgfxc (dotnet tool) 3.8.4.1 Компиляция шейдеров при сборке