CI / build-test (push) Successful in 1m16s
Browsers can't speak UDP, so WebSocket is the engine's one transport. The server side is a dependency-free RFC 6455 implementation over TcpListener (handshake, frame codec with masking and fragmentation, ping/pong — unit-tested against the RFC example vectors); the client wraps ClientWebSocket, which works on desktop and maps to the browser WebSocket in Blazor WASM. Both sit behind the poll-based INetConnection so simulation systems drain messages from their own thread; client sends are chained fire-and-forget (no blocking — wasm-safe). Replication is server-authoritative: games register unmanaged component types in a ReplicationSchema (same order both sides, up to 32 types), ReplicationServer snapshots entities carrying NetId once per send and ships each connection only the components that changed since its last snapshot — a reliable ordered transport needs no acks for deltas. New connections receive the full state through the same path; despawns are tracked by set difference. ReplicationClient applies snapshots to a local EntityStore and raises EntitySpawned so the game can decorate replicated entities with presentation components. Covered by 16 tests including a real loopback exchange between WebSocketClient and WebSocketServer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
419 lines
38 KiB
Markdown
419 lines
38 KiB
Markdown
# Архитектура mrgameeng
|
||
|
||
## Обзор
|
||
|
||
**mrgameeng** — кроссплатформенный 2D игровой движок на базе MonoGame 3.8.4
|
||
(DesktopGL: Windows, Linux, macOS), .NET 8, C#.
|
||
|
||
Ключевое архитектурное решение — **ECS-first**: движок плотно интегрирует
|
||
[Friflo.Engine.ECS](https://github.com/friflo/Friflo.Engine.ECS) (v3.6) как ядро
|
||
модели объектов. Это самая производительная ECS-библиотека в .NET по результатам
|
||
[Ecs.CSharp.Benchmark](https://github.com/Doraku/Ecs.CSharp.Benchmark): ноль аллокаций
|
||
в горячих путях, archetype-хранилище, встроенные системы, события и индексы.
|
||
|
||
## Принципы
|
||
|
||
1. **Данные отдельно от логики.** Всё состояние игры — в компонентах
|
||
(`struct : IComponent`), вся логика — в системах (`QuerySystem<T>`).
|
||
Никаких `Update()` у игровых объектов и иерархий наследования сущностей.
|
||
2. **Ноль аллокаций в кадре.** Системы, выполняющиеся каждый кадр, не должны
|
||
аллоцировать память. Проверяется бенчмарками и code review.
|
||
3. **Модульность.** Движок разбит на библиотеки по функциональным областям.
|
||
Игра подключает только то, что использует.
|
||
4. **Всё демонстрируется.** Каждая публичная возможность движка покрыта тестами
|
||
(где логика тестируема без GPU) и показана в игре
|
||
[LittleSim](https://gitea.hsrv.site/mrleo1nid/LittleSim) — живой витрине движка
|
||
(движок подключён туда сабмодулем `engine/`).
|
||
|
||
## Модули
|
||
|
||
Библиотеки сгруппированы по архитектурной роли, а не по одной на фичу. Каждая фича —
|
||
подпапка библиотеки-хоста со своим неймспейсом `MrGameEng.<Фича>` (неймспейс не
|
||
привязан к сборке), поэтому `using MrGameEng.Tilemaps;` и т.п. работают и после слияния.
|
||
|
||
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
||
|------------------------------|------------------------------------------------------------|
|
||
| `MrGameEng.Core` | Платформо-независимое ядро (зависит только от Friflo): `EngineContext`, `EntityStore`, `SystemRoot`, сцены и переходы (тайминг — `Transition`, `SceneManager`; визуал переходов — в `Host`), время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`; `Climate` — непрерывная сезонная/суточная температура и сезон поверх календаря, `context.UseClimate(...)`), жизненный цикл, `ServiceRegistry`, `Log`. **`HeadlessHost`** — цикл без окна и GPU на фиксированном тике (`Tick`/`RunTicks`/`Run` с realtime-пейсингом): дедикейтед-серверы, батч-симуляция, тесты |
|
||
| `MrGameEng.Host` | Оконный MonoGame-хост: `GameHost` (обёртка над `Game` — цикл, окно, `GraphicsDeviceManager`; публикует `GraphicsDevice` сервисом в контексте), визуальные переходы сцен (`OverlayTransition`, фабрики `Transitions.Fade/Wipe`, `TransitionRenderer`). **Input** (`MrGameEng.Input`, `Host/Input/`): клавиатура, мышь, геймпад, action maps |
|
||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер. **Lighting** (`MrGameEng.Lighting`, `Graphics/Lighting/`): амбиент день/ночь поверх `Calendar` → `Renderer2D.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.Net` | Мультиплеер, совместимый с браузером по построению: WebSocket-сервер (RFC 6455 поверх `TcpListener`, без зависимостей — браузер не умеет UDP, поэтому транспорт движка один — WebSocket), клиент на `ClientWebSocket` (работает в Blazor WASM), оба за poll-интерфейсом `INetConnection`; server-authoritative репликация компонентов: `ReplicationSchema` (unmanaged-компоненты, до 32 типов), `ReplicationServer` (пер-соединенческие дельты против последнего отправленного — ack не нужны поверх надёжного упорядоченного транспорта), `ReplicationClient` (применение снапшотов в локальный `EntityStore`), компонент `NetId` |
|
||
| `MrGameEng.UI` | Игровой UI на [Myra](https://github.com/rds1983/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.Host ─┐
|
||
MrGameEng.Audio ─┤
|
||
MrGameEng.Net ─┤
|
||
MrGameEng.Graphics ─┼──► MrGameEng.Core ──► Friflo.Engine.ECS
|
||
│
|
||
MrGameEng.Content ─┤
|
||
MrGameEng.Simulation ─┤ (Content — за Texture2DRegion,
|
||
MrGameEng.UI ─┴──► MrGameEng.Graphics Simulation/UI — за Transform2D/рендер)
|
||
```
|
||
|
||
Библиотека зависит **только от `Core` и `Graphics`**. `Core` зависит только от Friflo —
|
||
ни MonoGame, ни другой платформы: благодаря этому симуляция запускается и без окна
|
||
(`HeadlessHost`). MonoGame (`MonoGame.Framework.DesktopGL`) тянут платформенные и
|
||
графические библиотеки: `Host`, `Graphics`, `Audio` (и транзитивно их потребители).
|
||
Платформенные ресурсы (например `GraphicsDevice`) хосты публикуют сервисами в
|
||
`EngineContext.Services`; графический код достаёт девайс через
|
||
`context.GetGraphicsDevice()` (расширение в `Graphics`). На `Host` не зависит никто,
|
||
кроме самой игры — это точка входа оконной платформы. Если двум библиотекам нужен общий
|
||
тип — он переезжает в `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` — ингейм-консоль (тогглинг клавишей <code>`</code>):
|
||
|
||
- Перехватывает всё, что пишется через статический `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](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/`
|
||
проекта игры как сырые файлы (копируются в output при сборке) и грузятся в рантайме:
|
||
|
||
| Тип | Форматы | Как грузим |
|
||
|----------------|-----------|---------------------------------------------------------|
|
||
| Текстуры | png, jpg | `Texture2D.FromFile` |
|
||
| Шрифты | ttf | FontStashSharp (растеризация в рантайме, динамический атлас) |
|
||
| Звуки | wav | `SoundEffect.FromFile` |
|
||
| Музыка | ogg | Стриминг через NVorbis + `DynamicSoundEffectInstance` |
|
||
| Шейдеры | fx | Компиляция `dotnet-mgfxc` на этапе сборки (MSBuild target), в рантайме грузится байткод `.mgfx` |
|
||
|
||
`AssetManager` (в `MrGameEng.Assets`) грузит ресурсы по типизированному хендлу
|
||
`AssetRef<T>` (путь + тип), кэширует по пути и владеет временем жизни (Dispose
|
||
при выгрузке сцены/игры).
|
||
|
||
### Кодогенератор хендлов
|
||
|
||
`MrGameEng.Assets.Generator` — Roslyn **incremental source generator**. Файлы из
|
||
`Assets/` передаются ему через `AdditionalFiles` (один glob в `.csproj` игры).
|
||
По дереву папок он генерирует статический класс с типизированными хендлами,
|
||
тип выводится из расширения файла:
|
||
|
||
```csharp
|
||
// 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, Transitions.Fade(0.5f))` — переключение с визуальным переходом:
|
||
фаза закрытия (старая сцена живёт) → своп при полном покрытии → фаза открытия.
|
||
Тяжёлый `OnLoad` новой сцены скрыт за полностью закрытым экраном.
|
||
- Тайминг-машина (`Transition` — длительности фаз, покрытие) живёт в `Core` и работает
|
||
и в headless-контексте; визуал — в `Host`: встроенные `Transitions.Fade(duration, color)`
|
||
и `Transitions.Wipe(duration, color)` (шторка). Свои — наследованием от
|
||
`OverlayTransition` (рисование через `TransitionRenderer.Fill` в нормализованных
|
||
координатах экрана); `GameHost` рисует оверлей поверх сцены, читая
|
||
`Scenes.ActiveTransition`/`TransitionCoverage`/`TransitionPhase`.
|
||
- Переходы идут по **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](https://gitea.hsrv.site/mrleo1nid/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 | Компиляция шейдеров при сборке |
|