Document Faset architecture and roadmap through MVP

This commit is contained in:
Emil
2026-09-18 02:21:40 +03:00
commit 8f9d5c2969
40 changed files with 5791 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
# 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.