342 lines
30 KiB
Markdown
342 lines
30 KiB
Markdown
# 01. Архитектурные концепции
|
||
|
||
**Статус на 18.09.2026.** Исследование и проектные решения, не описание реализованного движка. Канонические решения — [ARCHITECTURE.md](../ARCHITECTURE.md), порядок до и после MVP — [PLAN.md](../../PLAN.md). Сведения о других движках сохраняются как исследовательский контекст; прежние рекомендации, помеченные **superseded**, не задают стек Faset.
|
||
|
||
Принято: C++ и EnTT runtime; собственный retained UI на C++ с декларативными layout/styles и тёмной темой по умолчанию, ImGui для отладки; SDL3, Vulkan 1.3, собственный renderer/RenderGraph, Slang; CMake + Ninja, Clang на Linux и clang-cl на Windows; Box2D/Box3D. Lua — обязательный следующий этап развития, необязательная зависимость конкретной игры. Первое MVP доводит маленькие 2D- и 3D-демо до самостоятельного экспорта на обе ОС; advanced graphics — после MVP.
|
||
|
||
## 1. Архитектура должна следовать пользовательским действиям
|
||
|
||
У движка есть два связанных, но не одинаковых продукта:
|
||
|
||
- **runtime** — запускает игру, симулирует мир и выводит кадр;
|
||
- **authoring environment** — помогает человеку (и при необходимости automation-инструментам) строить, исследовать и исправлять мир.
|
||
|
||
Авторские документы и работающая игра имеют разные владельцы состояния. Общая схема описывает сериализуемые данные, но редакторские транзакции не перехватывают каждое изменение компонента в игре.
|
||
|
||
```text
|
||
Editor UI / CLI / MCP / editor plugins
|
||
↓
|
||
AuthoringService: JSON documents, schema, revisions, transactions, Undo
|
||
↓ validated scene snapshot + cooked assets
|
||
Separate Player: statically linked C++ gameplay → EnTT runtime
|
||
↓
|
||
Physics / render extraction / renderer / runtime diagnostics
|
||
```
|
||
|
||
MCP работает только на стороне редактора: authoring, импорт, сборка, Play/Stop и editor logs. Он не читает и не редактирует runtime world, отсутствует в Player и экспортированных играх. Игровой C++ использует runtime API и command buffer; редакторские расширения используют AuthoringService. Snapshot запуска не меняется вслед за последующими правками исходной сцены.
|
||
|
||
## 2. Рекомендуемые слои
|
||
|
||
Не делать одну гигантскую `Engine`-сборку. Полезная граница модулей:
|
||
|
||
```text
|
||
Engine.Editor # окна, selection, gizmos, asset browser, commands
|
||
Engine.Authoring # scenes, prefabs, transactions, reflection, validation
|
||
Engine.Runtime # world, systems, events, scheduling, time, services
|
||
Engine.Spatial # transforms, bounds, hierarchy, queries, cameras
|
||
Engine.Physics # 2D/3D adapters, fixed simulation, contacts
|
||
Engine.Graphics # render extraction, render graph interface, resources
|
||
Engine.Assets # IDs, importer, cache, dependency graph, hot reload
|
||
Engine.Input # devices -> actions -> gameplay/UI contexts
|
||
Engine.Scripting # public scripting API, reload boundary, diagnostics
|
||
Engine.Platform # window, filesystem, threads, clocks, processes
|
||
Engine.Diagnostics # typed logs/traces; editor and runtime boundaries
|
||
Engine.Automation # editor-only MCP; authoring/import/build/PlayStop/logs
|
||
Engine.App # composition root and game-specific code
|
||
```
|
||
|
||
Направление зависимостей — сверху вниз только через стабильные interfaces/data contracts. `Editor` может ссылаться на `Authoring`, но runtime не должен зависеть от ImGui или конкретного editor UI. `Graphics.Vulkan` — backend `Graphics`, а не центр, от которого зависит gameplay.
|
||
|
||
### Правило владения
|
||
|
||
Каждый объект имеет одного явного владельца:
|
||
|
||
- authoring document владеет сохраняемыми объектами; runtime `World` — своими EnTT entities и компонентами;
|
||
- `AssetRegistry` владеет жизненным циклом загруженных ресурсов;
|
||
- `TransformGraph` владеет локальными/мировыми transform-вычислениями;
|
||
- physics backend владеет native body и рассчитанным transform динамического тела; teleport и kinematic movement задаются явно;
|
||
- renderer владеет GPU-копиями, но не авторским asset;
|
||
- editor transaction владеет историей изменений.
|
||
|
||
Ссылки между подсистемами — стабильные IDs/handles, а не сохраняемые указатели на C++-объекты. Это облегчает hot reload, сохранение, remote tools и диагностику dangling references.
|
||
|
||
## 3. World model: composition, а не одна иерархия
|
||
|
||
### Entity + components
|
||
|
||
Авторский объект имеет постоянный `ObjectId`; EnTT entity — временный handle внутри конкретного runtime world. Components — данные и явно определённые lifecycle hooks. Systems читают и изменяют наборы компонентов. Это дает композицию: объект может иметь `Transform2D`, `Sprite`, `Collider2D`, `AudioSource`, `Script` или их 3D-аналоги без дерева наследования.
|
||
|
||
O3DE описывает тот же принцип как «has-a», а не «is-a», и разделяет runtime/editor/system components ([O3DE ECS overview](https://www.docs.o3de.org/docs/user-guide/programming/components/overview)). Это полезное различие для нового движка:
|
||
|
||
- `TransformComponent` runtime существует в игре;
|
||
- `TransformEditorComponent` добавляет gizmo и authoring metadata;
|
||
- `AssetSystem`/`UndoSystem` — system services, не компоненты сущности.
|
||
|
||
### Отдельный transform graph
|
||
|
||
Не смешивать entity hierarchy с универсальным механизмом поведения. Нужна отдельная иерархия parent/child только там, где она имеет смысл:
|
||
|
||
- локальные transform для руки, камеры, дочернего объекта;
|
||
- UI layout;
|
||
- skeleton/кости;
|
||
- 2D-вложенность и canvas layers.
|
||
|
||
Логика gameplay, ownership ресурсов, physics constraints и spatial partition — другие графы. Один parent не должен неявно означать все типы связи. Это предотвращает циклы и неожиданное каскадное изменение.
|
||
|
||
### Авторская сущность и runtime entity
|
||
|
||
Сохраненная сцена содержит authoring IDs и компоненты. При запуске может появиться runtime entity mapping:
|
||
|
||
```text
|
||
AuthoringId 42 -> RuntimeEntity 918
|
||
PrefabId -> instance overrides
|
||
AssetId -> loaded resource handle
|
||
```
|
||
|
||
Это позволяет Player менять runtime без порчи исходной сцены. Mapping полезен внутренней диагностике, но не создаёт runtime MCP API; обратное применение произвольного состояния игры в документы не входит в MVP.
|
||
|
||
## 4. Scenes, prefabs и resources
|
||
|
||
Godot делает сцену универсальной композиционной единицей: сценой может быть weapon, character, door или уровень, а вложенные сцены ведут себя как переиспользуемые композиции ([design philosophy](https://docs.godotengine.org/en/stable/getting_started/introduction/godot_design_philosophy.html), [key concepts](https://docs.godotengine.org/en/stable/getting_started/introduction/key_concepts_overview.html)). Unity решает близкую задачу через prefab: GameObject с компонентами можно сохранять, вкладывать и делать вариации ([Prefabs](https://docs.unity3d.com/6000.1/Documentation/Manual/Prefabs.html)).
|
||
|
||
Для нового движка полезно объединить сильные стороны:
|
||
|
||
- **Scene** — редактируемое дерево/граф entities, которое можно открыть и запустить отдельно;
|
||
- **Prefab** — сцена как reusable asset с instance overrides;
|
||
- **Resource** — именованный типизированный asset (mesh, texture, material, animation, sound, script, data asset);
|
||
- **Spawnable** — подготовленная runtime-последовательность создания prefab/scene с dependency manifest.
|
||
|
||
### Принятые правила экземпляров и overrides
|
||
|
||
1. Шаблон — отдельный scene resource с постоянными `ObjectId` и `ComponentId`. Размещение хранит `TemplateAssetId`, собственный `InstanceId` и override layer; имя — только подпись.
|
||
2. Идентичность унаследованного объекта определяется цепочкой `InstanceId` вложенных размещений и исходным `ObjectId` в документе шаблона. Это цепочка инстанцирования, не путь transform-родителей: допустимый reparent не меняет ID.
|
||
3. Patch адресуется через instance chain, `ObjectId`, `ComponentId`, `FieldId`; `TypeId` проверяет совместимость. В v1 массив изменяется целиком, без хрупких index/name-based patches.
|
||
4. Порядок разрешения: данные шаблона → overrides вложенного экземпляра в содержащем шаблоне → overrides внешнего экземпляра в сцене. Последний явный override побеждает, включая намеренное совпадение с текущим default; неизменённые поля не записываются.
|
||
5. Добавленный объект или компонент получает новый ID в документе-владельце override. Сохраняются provenance и ссылка на родителя. Дублирование экземпляра создаёт новый `InstanceId`, переназначает внутренние ссылки и сохраняет внешние.
|
||
6. Reparent в v1 разрешён внутри одного экземпляра; циклы и перенос через границу вложенного экземпляра отклоняются. Перемещение экземпляра целиком допустимо. Команда явно выбирает сохранение local или world transform.
|
||
7. Удаление унаследованного объекта записывает suppression объекта и его разрешённого поддерева; шаблон остаётся прежним. Локально добавленный объект удаляется из документа-владельца. Ссылки и другие patches на подавленные цели требуют разрешения конфликта, а не молчаливого удаления.
|
||
8. Новая версия шаблона обновляет поля без overrides. Совместимые явные overrides сохраняются. Исчезнувшая цель, несовместимый тип, цикл и повреждённая обязательная ссылка дают конфликт; активной остаётся последняя согласованная версия до исправления.
|
||
9. Структурный batch проверяет revision, строит кандидат, валидирует итоговые связи и публикует один результат с одним Undo. `Revert` удаляет выбранный override и возвращает нижележащее значение; UI и MCP используют один authoring contract.
|
||
10. В MVP входят обычные и ациклично вложенные экземпляры, overrides полей, добавление объектов/компонентов, suppression и ограниченный reparent. Variant inheritance, `Apply overrides to template`, перенос через границы экземпляров и поэлементное слияние массивов — после MVP.
|
||
|
||
Inspector показывает источник и локальные отличия, переход к шаблону, Revert и конкретные конфликтующие targets. Импортированный шаблон использует тот же принцип разделения baseline и пользовательского слоя; детали публикации поколения — в [исследовании импорта](17-asset-pipeline-and-blender-roundtrip.md).
|
||
|
||
### Два режима связи
|
||
|
||
- **strong reference** — asset должен присутствовать; ошибка блокирует export или явно видна;
|
||
- **soft reference** — путь/ID загружается по требованию; полезен для больших уровней и optional content.
|
||
|
||
References хранятся по `AssetId`, а путь — метаданные/alias. Переименование файла не должно ломать проект.
|
||
|
||
## 5. 2D и 3D: единое ядро, специализированные представления
|
||
|
||
Не выбирать между «два полностью отдельных движка» и «2D — это 3D с нулевой координатой». Нужны три уровня:
|
||
|
||
### Общие концепции
|
||
|
||
- entity, components, resources, scenes, prefabs;
|
||
- transform hierarchy и local/world conversion;
|
||
- camera, visibility, layers/masks;
|
||
- input actions, animation, audio, particles;
|
||
- fixed/variable time, events, diagnostics;
|
||
- serialization, asset references, editor commands.
|
||
|
||
### Специализированные типы
|
||
|
||
| Общее | 2D | 3D |
|
||
|---|---|---|
|
||
| Transform | `Vector2`, `Rotation2D`, depth/layer | `Vector3`, quaternion, parent basis |
|
||
| Renderable | Sprite, tilemap, canvas item, 2D text | mesh, skinned mesh, terrain, 3D text |
|
||
| Camera | orthographic + canvas transform | perspective/orthographic + frustum |
|
||
| Physics | 2D bodies/shapes/joints | 3D bodies/shapes/joints |
|
||
| Spatial query | rect/grid/quadtree | AABB/BVH/octree/grid |
|
||
| Lighting | 2D lights/masks, optional normal maps | lights/shadows/environment |
|
||
|
||
Godot прямо отмечает, что APIs и tutorial patterns 2D/3D во многом аналогичны, но имеет отдельные Node2D/Node3D и физические пространства ([Introduction to 3D](https://docs.godotengine.org/en/stable/tutorials/3d/introduction_to_3d.html), [physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html)). Это хороший UX-паттерн: одинаковые названия и операции, но не скрывать важные различия.
|
||
|
||
### Гибридные игры — критерий архитектуры
|
||
|
||
После двух базовых MVP-демо проверять расширенные сочетания:
|
||
|
||
- 2D gameplay на 3D backdrop;
|
||
- 3D world-space UI и 2D overlay;
|
||
- orthographic camera в 3D world;
|
||
- Sprite/quad в 3D;
|
||
- particles и audio независимо от dimensionality.
|
||
|
||
Это заставляет правильно отделить world transform, camera projection и render representation.
|
||
|
||
## 6. Game loop и расписание систем
|
||
|
||
Минимально понятный цикл:
|
||
|
||
```text
|
||
FrameStart
|
||
input.poll_and_buffer()
|
||
fixed_accumulator += real_delta
|
||
ticks = 0
|
||
while fixed_accumulator >= fixed_dt and ticks < max_catch_up_ticks:
|
||
runtime_commands.apply_structural_barrier()
|
||
input.consume_for_tick()
|
||
gameplay.fixed_update(fixed_dt)
|
||
physics.apply_body_commands()
|
||
physics.step_and_wait(fixed_dt)
|
||
physics.readback_transforms_and_queue_events()
|
||
gameplay.process_physics_events_and_reactions()
|
||
fixed_accumulator -= fixed_dt
|
||
ticks += 1
|
||
discard_excess_whole_ticks_with_diagnostic(fixed_accumulator)
|
||
gameplay.update(variable_delta)
|
||
display_state.prepare_interpolated(previous_tick, current_tick,
|
||
fixed_accumulator / fixed_dt)
|
||
gameplay.late_update(display_state, variable_delta)
|
||
render.extract_snapshot(display_state)
|
||
render.submit()
|
||
diagnostics.end_frame()
|
||
FrameEnd
|
||
```
|
||
|
||
Structural commands, созданные во время tick, применяются только на барьере следующего tick, включая несколько ticks одного кадра. Начальная загрузка проходит отдельный явный барьер инициализации. Начальный лимит catch-up — четыре ticks; лишние целые интервалы отбрасываются с диагностикой, дробный остаток сохраняется. После pause/reset накопитель сбрасывается. Interpolated display state отделён от симуляции: Update/LateUpdate не переписывают рассчитанный physics transform динамического тела.
|
||
|
||
Physics и код, зависящий от столкновений, должны идти на fixed step; render/UI — variable step. Godot документирует отдельные `_physics_process` и `_process`, причем physics rate независим от framerate ([Idle and Physics Processing](https://docs.godotengine.org/en/stable/tutorials/scripting/idle_and_physics_processing.html)). Unity также подчеркивает, что `FixedUpdate` может вызываться несколько раз за кадр или не вызываться между кадрами и что physics simulation имеет определенное место в loop ([execution order](https://docs.unity3d.com/6000.5/Documentation/Manual/execution-order.html)).
|
||
|
||
### Не обещать ложную determinism
|
||
|
||
Fixed timestep стабилизирует расписание, но не делает автоматически всю физику детерминированной. Godot прямо предупреждает, что физика не гарантированно детерминирована ([physics introduction](https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html)). Для Faset fixed physics и variable frame являются базой; следующие режимы описывают дальнейшие возможности:
|
||
|
||
- `Realtime` — максимально плавный runtime;
|
||
- `Fixed` — фиксированный simulation step;
|
||
- `Record/Replay` — записывать input и seed, а не обещать byte-for-byte replay без теста backend;
|
||
- `Lockstep` — отдельная будущая возможность с жесткими ограничениями.
|
||
|
||
Системы имеют декларативные зависимости (`reads/writes`, `before/after`), а не неявный порядок регистрации. Unreal показывает практическую ценность tick groups и tick dependencies для physics/gameplay ordering ([Actor Ticking](https://dev.epicgames.com/documentation/unreal-engine/actor-ticking-in-unreal-engine)). Для нового движка достаточно сначала детерминированных фаз, затем parallel scheduling.
|
||
|
||
## 7. ECS, объектный код и data-oriented design
|
||
|
||
ECS дает cache-friendly iteration и уменьшает связанность при больших однородных наборах. Но это не религия и не универсальный public API. Data-oriented design — выбор layout по access patterns, а не обязательное использование archetype ECS ([ECS FAQ](https://github.com/SanderMertens/ecs-faq)). Game Programming Patterns отдельно рассматривает Component, Event Queue, Data Locality, Game Loop и другие паттерны ([catalog](https://gameprogrammingpatterns.com/contents.html)).
|
||
|
||
**Принято для Faset:** EnTT runtime, typed C++ gameplay API, явная metadata schema с постоянными `TypeId`/`FieldId`, удобные object/component descriptors в редакторе. Layout горячих данных и batching выбираются по измерениям. EnTT handles не записываются в JSON.
|
||
|
||
**Superseded, история сравнения:** первоначально предлагались Flecs/archetype queries и C# façade. Они не входят в выбранный стек. Lua добавляется после C++-основы отдельным модулем; наличие схемы само по себе не реализует Lua bindings.
|
||
|
||
### Сигналы и события
|
||
|
||
Не вызывать произвольные методы десятка объектов во время системного прохода. Использовать typed events/commands:
|
||
|
||
- события контактов, input actions, asset loaded;
|
||
- command buffer для structural changes;
|
||
- double-buffered event queues, чтобы producer и consumer имели понятный кадр доставки;
|
||
- логирование origin/target/sequence для отладки.
|
||
|
||
Event Queue и Double Buffer — проверенные декомпозирующие паттерны, но их нужно ограничивать schema и lifetime ([Game Programming Patterns](https://gameprogrammingpatterns.com/contents.html)).
|
||
|
||
## 8. Asset pipeline: source не равен runtime
|
||
|
||
Pipeline должен быть видимым графом:
|
||
|
||
```text
|
||
source file + importer settings + engine version
|
||
│
|
||
▼
|
||
importer/cache
|
||
│
|
||
typed artifact + dependencies
|
||
│
|
||
▼
|
||
cooked platform package
|
||
```
|
||
|
||
Godot импортирует файлы в скрытые внутренние ресурсы и предупреждает, что ResourceLoader учитывает импорт, тогда как прямой FileAccess может работать в editor, но сломаться в export ([import process](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html)). Unity разделяет source asset, `.meta` с идентичностью и кэшируемые artifacts, которые можно регенерировать ([Asset Workflow](https://docs.unity3d.com/2019.3/Documentation/Manual/AssetWorkflow.html), [Asset Database](https://docs.unity3d.com/2020.3/Documentation/Manual/AssetDatabaseCustomizingWorkflow.html)). O3DE называет преобразование source в optimized product assets Asset Pipeline и выполняет его Asset Processor ([key concepts](https://docs.o3de.org/docs/welcome-guide/key-concepts)).
|
||
|
||
### Минимальный контракт importer
|
||
|
||
```text
|
||
ImporterId
|
||
Input extensions
|
||
Settings schema + defaults
|
||
Output asset type(s)
|
||
Dependency discovery
|
||
Content hash / importer version
|
||
Diagnostics (error/warning/info + source span)
|
||
Cancel/progress
|
||
Deterministic output
|
||
```
|
||
|
||
Нужно хранить рядом или в проектном manifest:
|
||
|
||
- стабильный `AssetId`;
|
||
- importer settings, не только результат;
|
||
- dependency graph;
|
||
- source hash и importer version;
|
||
- platform variants;
|
||
- last import diagnostics.
|
||
|
||
Ошибка импорта — first-class asset state: красный badge, понятная причина, путь к offending dependency и кнопка retry/open log.
|
||
|
||
## 9. Serialization и эволюция формата
|
||
|
||
Авторские сцены и data assets должны быть:
|
||
|
||
- текстовыми и diff-friendly;
|
||
- с явными типами и версиями schema;
|
||
- устойчивыми к reorder полей;
|
||
- способными сохранять неизвестные поля хотя бы в compatibility window;
|
||
- защищенными от случайного хранения runtime handles, GPU pointers и absolute paths.
|
||
|
||
Принят JSON с постоянными object/resource IDs, `TypeId`, `FieldId` и schema versions. Имена типов и полей могут сопровождать данные для чтения человеком, но не являются ключами идентичности. Конкретный envelope определяется реализацией и fixtures миграции; прежний YAML-пример с name-based references **superseded**.
|
||
|
||
Binary artifacts допустимы для cooked output и больших blobs, но исходный truth должен быть читаемым. Autosave пишет recovery snapshot, а не перезаписывает source без undo/history.
|
||
|
||
Миграция schema — отдельная команда, dry-run preview и резервный backup. Версию формата не связывать жестко с версией renderer.
|
||
|
||
## 10. Потоки и async
|
||
|
||
Безопасная базовая модель для личного engine:
|
||
|
||
- AuthoringService сериализует изменения документов; runtime world отдельно владеет structural changes EnTT в разрешённых фазах;
|
||
- worker threads импортируют assets, компилируют shaders, готовят jobs;
|
||
- physics/render native calls выполняются на thread, который требует backend;
|
||
- cross-thread изменения приходят через typed queue и применяются в определенной фазе;
|
||
- cancellation и progress обязательны для долгих задач.
|
||
|
||
Не делать «все async» API без ясной точки commit. Loader может background-load, но переход `Loading → Ready` должен быть наблюдаемым и происходить в transaction-safe фазе.
|
||
|
||
## 11. Расширения и backend abstraction
|
||
|
||
Редакторские C++ plugins собираются как DLL/SO под точную версию SDK/toolchain; стабильный произвольный C++ ABI и hot-unload не обещаются. Gameplay статически включается в отдельный Player. Регистры расширений могут добавлять:
|
||
|
||
- component/schema;
|
||
- system;
|
||
- importer;
|
||
- editor panel/tool;
|
||
- export step;
|
||
- render feature/backend.
|
||
|
||
Плагин не должен подменять core ID/lifetime/serialization conventions. Registry должен проверять версии API, зависимости и capability.
|
||
|
||
Backend interface абстрагирует window/surface/device/resource submission, но не должен прятать концепции, которые нужны debugging. Например, render graph может сообщать pass/resource barriers в trace, даже если gameplay видит только `Renderable`.
|
||
|
||
## 12. Диагностика и тестируемость — не надстройка
|
||
|
||
Минимальная observability schema:
|
||
|
||
- structured log: category, severity, entity/asset ID, frame, correlation ID;
|
||
- frame trace: systems, durations, allocations, render passes;
|
||
- asset trace: importer, dependencies, cache hit/miss;
|
||
- authoring snapshot/diff; runtime traces остаются отдельной внутренней диагностикой и не публикуются через MCP;
|
||
- command journal: кто, что, когда и почему изменил;
|
||
- validation: missing resources, invalid transforms, duplicate IDs, unowned native handles.
|
||
|
||
Тестовые уровни:
|
||
|
||
1. pure math/serialization/schema migrations;
|
||
2. system tests на headless world;
|
||
3. asset importer golden tests;
|
||
4. physics adapter tests с tolerance;
|
||
5. render extraction tests без GPU;
|
||
6. GPU/backend smoke tests;
|
||
7. editor command/undo integration tests;
|
||
8. маленькие end-to-end projects: 2D, 3D, hybrid.
|
||
|
||
Главная цель — ошибка из графического кадра должна быть трассируема обратно к asset/entity/component/command.
|