Files
mrgameeng/docs/architecture.md
T
Leonid PershinandClaude Opus 4.8 79d406a9f4
CI / build-test (push) Successful in 1m14s
Add 2D lightmap: occlusion shadows and point lights
Extend the Lighting module with a per-cell lightmap. LightmapBuilder (pure,
tested) fills an ambient base, shades occluder cells, and adds point lights
that attenuate with distance and are blocked by occluders between source and
cell (grid-traced soft shadows). Lightmap holds the grid plus a greyscale
texture (Upload) and a bilinear SampleAt for the simulation; a PointLight
component places lights in the world. LightmapSystem rebuilds the grid a few
times a second (ambient from the day/night cycle with a night floor, occluders
from a game-supplied grid, point lights from the ECS); LightmapRenderSystem
multiplies the lightmap over the world after the sprite flush and under the
HUD. Wired via scene.UseLighting(...), sampled through Lighting.SampleAt. Docs
updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:40:17 +03:00

35 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. Всё демонстрируется. Каждая публичная возможность движка покрыта тестами (где логика тестируема без GPU) и показана в игре LittleSim — живой витрине движка (движок подключён туда сабмодулем engine/).

Модули

Библиотеки сгруппированы по архитектурной роли, а не по одной на фичу. Каждая фича — подпапка библиотеки-хоста со своим неймспейсом MrGameEng.<Фича> (неймспейс не привязан к сборке), поэтому using MrGameEng.Tilemaps; и т.п. работают и после слияния.

Библиотека (сборка) Фичи (неймспейсы) и ответственность
MrGameEng.Core Игровой цикл (хост над Game), EntityStore, SystemRoot, сцены, время (GameClock.TimeScale; GameSpeed — дискретная скорость пауза/1×/3×/6× поверх часов; Calendar — игровые дни поверх масштабированного времени, context.UseCalendar(...); Climate — непрерывная сезонная/суточная температура и сезон поверх календаря, context.UseClimate(...)), жизненный цикл. Input (MrGameEng.Input, Core/Input/): клавиатура, мышь, геймпад, action maps
MrGameEng.Graphics Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. Tilemaps (MrGameEng.Tilemaps, Graphics/Tilemaps/): тайловые карты кодом — TileGrid + TileSet + компонент Tilemap, отрисовка видимых клеток через батчер. Lighting (MrGameEng.Lighting, Graphics/Lighting/): амбиент день/ночь поверх CalendarRenderer2D.AmbientLight, scene.UseDayNight(renderer); по-клеточный лайтмап — LightmapBuilder (амбиент × окклюзия + точечные PointLight с трассировкой теней), накладывается multiply поверх мира через scene.UseLighting(...), сэмплируется Lighting.SampleAt
MrGameEng.Audio Звуковые эффекты и музыка (NVorbis); AudioManager с SoundVolume/MasterVolume (одна ручка на эффекты и музыку)
MrGameEng.Content Пайплайн контента. Assets (MrGameEng.Assets): runtime-загрузка без Content Pipeline, кэш, AssetRef<T>. Atlases (MrGameEng.Atlases): текстурные атласы — сборка из дерева картинок (AtlasBuilder) и рантайм-загрузка (TextureAtlas), CLI tools/MrGameEng.AtlasTool. Mods (MrGameEng.Mods): система модов — порядок загрузки, JSON-дефы, локализация, слияние деревьев контента
MrGameEng.Simulation Детерминированные геймплей-примитивы без данных мира. Pathfinding (MrGameEng.Pathfinding): A*, Dijkstra, BFS, flow fields по гриду. AI (MrGameEng.AI): utility-ИИ — кривые отклика, соображения, действия, выбор (UtilityAi<TContext>), Blackboard. Collisions (MrGameEng.Collisions): компонент Collider, spatial hash, пары/запросы/raycast
MrGameEng.UI Игровой UI на Myra: Desktop на сцену, виджеты, скининг. DevConsole (MrGameEng.DevConsole): ингейм-консоль — логи Log, команды, история, автодополнение. Inspector (MrGameEng.Inspector): ECS-дебагер в духе Chrome DevTools — дерево сущностей по архетипам, компоненты/поля с правкой простых полей, выбор кликом по миру с подсветкой, вкладка перфа рендера (scene.UseInspector(renderer), F1)
MrGameEng.Assets.Generator Roslyn incremental source generator: классы с типизированными хендлами ресурсов (отдельный анализатор netstandard2.0)

Планируемые области (по мере развития): Physics2D, Particles — добавляются как подпапки подходящей библиотеки или новой библиотекой, если это новая роль/тяжёлая зависимость.

MrGameEng.Mods

Контент игры описывается модами — папками вида Mods/<Id> с метаданными в About/About.json (id, имя, версия, зависимости). ModLoader.Load упорядочивает моды детерминированно: зависимости раньше зависимых, при равенстве — по алфавиту id. Поздний мод переопределяет ранние во всех системах контента. Сама игра поставляет свой контент как мод Core — любой другой мод может переопределить её данные.

  • Дефы (Defs/**/*.json): файл-конверт { "type": "<ключ>", "defs": [ … ] }; CLR-тип на ключ регистрирует игра (DefDatabase.RegisterType<T>). Поля: defName (уникален в типе; одноимённый деф позднего мода полностью заменяет ранний), parent (поля родителя как основа, свои — поверх; вложенные объекты заменяются целиком), abstract (только родитель, в базу не попадает; не наследуется), label.
  • Локализация (Languages/<код>/**/*.json): плоские словари ключ→строка; LanguageManager переключает язык на лету, недостающие ключи берёт из языка по умолчанию, в крайнем случае возвращает сам ключ.
  • Деревья контента: ModContentTree.Build(mods, "Textures") сливает одноимённые папки всех модов (поздний мод побеждает по относительному пути) — результат кормится, например, в AtlasBuilder.Build(options, sources) для инкрементальной сборки атласов при старте игры.

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

MrGameEng.Audio       ─┐
MrGameEng.Graphics    ─┼──► MrGameEng.Core ──► MonoGame.Framework.DesktopGL
                       │                   └──► Friflo.Engine.ECS
MrGameEng.Content     ─┤
MrGameEng.Simulation  ─┤                         (Content — за Texture2DRegion,
MrGameEng.UI          ─┴──► MrGameEng.Graphics    Simulation/UI — за Transform2D/рендер)

Библиотека зависит только от Core и Graphics. Core зависит только от MonoGame и Friflo. Если двум библиотекам нужен общий тип — он переезжает в Core (или, если это графический тип, в Graphics). Content тянет Graphics (атласы выдают Texture2DRegion); Simulation тянет Graphics (Collisions использует Transform2D, RectF); UI — только Core (Myra рисует своим SpriteBatch).

Фичи, собранные в одну библиотеку, делят её набор пакетов (например, Content несёт и FontStash, и StbImage). Тяжёлые/опциональные зависимости (Myra, NVorbis) держим в отдельных библиотеках, чтобы остальной движок их не тянул.

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

Консоль разработчика

MrGameEng.DevConsole — ингейм-консоль (тогглинг клавишей `):

  • Перехватывает всё, что пишется через статический MrGameEng.Core.Log (Debug/Info/Warning/Error; движок логирует ключевые события сам).
  • Команды: console.Register(name, description, handler); встроенные — help, clear, echo, timescale, close, quit; игра добавляет свои.
  • История ввода (↑/↓), автодополнение по Tab, скролл (PageUp/PageDown/End/колесо).
  • Производительность: кольцевой буфер строк (2048), отрисовка — один Label, текст которого перестраивается только при изменении (Revision); при закрытой консоли — ноль работы в кадре.
  • Ядро (DevConsole) — чистая логика без Myra, полностью покрыта headless-тестами; Myra-обвязка отдельно. scene.UseDevConsole() — последним в OnLoad, чтобы консоль рисовалась поверх UI. Сервис общий, живёт между сценами.

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);

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

Текстурные атласы

MrGameEng.Atlases превращает дерево отдельных картинок в атласы и грузит их в рантайме. Спрайты с регионами одной страницы атласа батчер сливает в один draw call.

Сборка (билд-тайм, без GPU)

AtlasBuilder.Build(AtlasBuildOptions) — чистый CPU (StbImageSharp/StbImageWriteSharp):

  • Источник сканируется рекурсивно (png/jpg/jpeg/bmp); картинки группируются в атласы по первым GroupDepth папкам относительного пути (0 — один атлас на всё).
  • Упаковка — детерминированный shelf-packer (ShelfPacker): сортировка по высоте, полки, страницы до MaxPageSize² (по умолчанию 2048), зазор Padding (2 px), размер страницы подрезается до степени двойки; негабаритные картинки получают отдельную страницу под себя.
  • Выход: страницы <Имя>.atlas.<N>.png + метаданные <Имя>.atlas (JSON: страницы, регионы с ключами и прямоугольниками). Ключ региона — путь от корня источника без расширения (Things/Pawn/Animal/Fox).
  • Инкрементальность: группа пересобирается только если изменились исходники, состав файлов или параметры сборки; атласы исчезнувших групп удаляются из выходной папки.

CLI-обёртка: dotnet run --project tools/MrGameEng.AtlasTool -- <источник> <выход> [--group-depth N] [--page-size N] [--padding N] [--root-name Имя] [--force].

Загрузка (рантайм)

  • context.UseTextureAtlases() регистрирует загрузчик TextureAtlas в AssetManager; кодогенератор выдаёт хендлы AssetRef<TextureAtlas> для файлов .atlas (страницы *.atlas.N.png собственных Texture2D-хендлов не получают).
  • TextureAtlas владеет страницами (premultiplied alpha, как все текстуры движка) и отдаёт регионы: atlas.GetRegion("Things/Pawn/Animal/Fox")Texture2DRegion, готовый для Sprite.

Тайловые карты

MrGameEng.Tilemaps — тайловые карты, создаваемые кодом (загрузка Tiled — в бэклоге):

  • TileSet — словарь тайлов: Add(region, tint?) возвращает id; id 0 зарезервирован под «пусто». Регион + тинт позволяют строить тайлсеты и из текстур атласа, и из тонированной белой текстуры.
  • TileGrid — плотная сетка ushort-id с проверкой границ; заполняется генератором мира, мутируется в рантайме (изменение видно со следующего кадра).
  • Компонент Tilemap (struct : IComponent): грид + тайлсет + Origin, TileSize, слой, Depth, общий тинт карты. Обычная сущность — карт может быть несколько (земля, декор поверх).
  • scene.UseTilemaps() (после UseRenderer2D()) вставляет TilemapRenderSystem в Draw-фазу перед flush. Система считает видимый диапазон клеток по cull-rect камеры (TilemapMath.VisibleCells) и сабмитит только его: стоимость кадра зависит от экрана, а не от размера грида. Тайлы батчатся со спрайтами по обычному порядку слой → depth → текстура: пол из одной текстуры атласа — один draw call.

Поиск пути

MrGameEng.Pathfinding — алгоритмы поиска пути по гриду; зависит только от Core и не владеет данными мира — игра реализует IPathGrid (Width/Height, IsPassable, Cost ≥ 1) поверх своего рельефа или TileGrid.

  • GridPathfinder.FindPath(start, goal, path, algorithm)A* (octile/Manhattan эвристика), Dijkstra (с учётом цены клеток) и BFS (быстрейший для равномерных гридов, цену игнорирует). Связность 4 или 8; диагонали никогда не срезают углы.
  • FlowFieldBuilder.Build(goals, field) — multi-source Dijkstra строит flow field: дистанция + нормализованное направление на клетку. Толпа любого размера дальше стоит O(1) на агента в кадр — основной инструмент для масс агентов (LittleSim).
  • Оптимизация под ECS: все буферы созданы один раз под размер грида и инвалидируются generation-штампом — повторные запросы не аллоцируют и не чистят массивы. Один экземпляр на систему; результаты детерминированы.

ИИ агентов (utility)

MrGameEng.AI — примитивы для принятия решений агентами (жители LittleSim). Зависит только от Core, не владеет данными мира и не привязан к ECS: всё параметризовано контекстом TContext, который игра передаёт сама (снимок восприятия, хендл сущности, blackboard — что угодно). Основан на Infinite-Axis Utility System:

  • ResponseCurve (value type) — кривая отклика, нормализованный вход [0,1] → полезность [0,1]: Linear, Polynomial (степень), Logistic (S-кривая), SmoothStep. Вход и выход клампятся. Внимание: default(ResponseCurve) имеет нулевой наклон (всегда 0) — для тождества используйте ResponseCurve.Identity.
  • Consideration<TContext> — одно соображение: читает сырое значение из контекста, нормирует по диапазону [min,max] и прогоняет через кривую.
  • UtilityAction<TContext> — действие из набора соображений. Очки = произведение соображений × Weight; любой ноль ветирует действие. Компенсирующий множитель (make-up value) убирает смещение произведения многих факторов вниз.
  • UtilityAi<TContext> — reasoner: Select (детерминированно лучшее действие, при равенстве — первое) и SelectWeighted(random) (рулетка по очкам для разнообразия, воспроизводимо при seed). Очки пишутся в переиспользуемый буфер — повторные вычисления не аллоцируют; один экземпляр на вид агента, не потокобезопасен.
  • Blackboard — типизированная рабочая память агента (Set/TryGet/GetOrDefault) для холодных путей (восприятие, планирование).

Витрина в LittleSim: PawnDecisionSystem выбирает «бродить/отдыхать» по энергии жителя, PawnNeedsSystem тратит/восстанавливает энергию, уставшие темнеют (PawnAppearanceSystem). Команда консоли ai [energy] печатает очки и расклад отдыхающих/блуждающих.

Коллизии

MrGameEng.Collisions — определение столкновений (без разрешения физики — она в бэклоге):

  • Компонент Collider (struct : IComponent): круг или AABB (без вращения), Offset, битовые маски Layer/CollidesWith (пара регистрируется, только если маски согласны в обе стороны). Создание через Collider.Circle(r) / Collider.Box(w,h).
  • CollisionWorld — uniform spatial hash на плоских массивах (головы бакетов + связные списки индексов), перестраивается с нуля каждый тик за O(n): для движущихся сущностей это дешевле инкрементальных обновлений. Ноль аллокаций после прогрева; порядок пар детерминирован (порядок чанков Friflo).
  • scene.UseCollisions(cellSize) добавляет CollisionSystem — регистрируй её после систем движения; системы, читающие world.Pairs, — после неё.
  • Запросы для игровых систем: Pairs (пересекающиеся пары кадра), QueryAabb(rect, span), Raycast(from, to, mask) (ближайшее попадание).
  • cellSize подбирается под типичный размер коллайдера; крупные коллайдеры занимают несколько ячеек — корректность не страдает, страдает константа.

Рендеринг

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 аллокаций на кадр ниже порога параллелизма. Эталонный замер (стресс-сцена, до переноса демо в LittleSim): 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.*)
tests/      xUnit-тесты, по проекту на библиотеку
tools/      CLI-инструменты (упаковщик атласов)
docs/       документация (русский)

Демо-проекта в движке нет: витрина движка — игра LittleSim (движок подключён туда сабмодулем engine/).

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

Зависимость Версия Назначение
.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
StbImageSharp 2.30.15 Декодирование картинок при сборке атласов
StbImageWriteSharp 1.16.7 Запись PNG-страниц атласов
dotnet-mgfxc (dotnet tool) 3.8.4.1 Компиляция шейдеров при сборке