Files
Faset_Engine/docs/studies/01-architecture.md
T

342 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.