30 KiB
01. Архитектурные концепции
Статус на 18.09.2026. Исследование и проектные решения, не описание реализованного движка. Канонические решения — ARCHITECTURE.md, порядок до и после MVP — 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-инструментам) строить, исследовать и исправлять мир.
Авторские документы и работающая игра имеют разные владельцы состояния. Общая схема описывает сериализуемые данные, но редакторские транзакции не перехватывают каждое изменение компонента в игре.
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-сборку. Полезная граница модулей:
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). Это полезное различие для нового движка:
TransformComponentruntime существует в игре;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:
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, key concepts). Unity решает близкую задачу через prefab: GameObject с компонентами можно сохранять, вкладывать и делать вариации (Prefabs).
Для нового движка полезно объединить сильные стороны:
- Scene — редактируемое дерево/граф entities, которое можно открыть и запустить отдельно;
- Prefab — сцена как reusable asset с instance overrides;
- Resource — именованный типизированный asset (mesh, texture, material, animation, sound, script, data asset);
- Spawnable — подготовленная runtime-последовательность создания prefab/scene с dependency manifest.
Принятые правила экземпляров и overrides
- Шаблон — отдельный scene resource с постоянными
ObjectIdиComponentId. Размещение хранитTemplateAssetId, собственныйInstanceIdи override layer; имя — только подпись. - Идентичность унаследованного объекта определяется цепочкой
InstanceIdвложенных размещений и исходнымObjectIdв документе шаблона. Это цепочка инстанцирования, не путь transform-родителей: допустимый reparent не меняет ID. - Patch адресуется через instance chain,
ObjectId,ComponentId,FieldId;TypeIdпроверяет совместимость. В v1 массив изменяется целиком, без хрупких index/name-based patches. - Порядок разрешения: данные шаблона → overrides вложенного экземпляра в содержащем шаблоне → overrides внешнего экземпляра в сцене. Последний явный override побеждает, включая намеренное совпадение с текущим default; неизменённые поля не записываются.
- Добавленный объект или компонент получает новый ID в документе-владельце override. Сохраняются provenance и ссылка на родителя. Дублирование экземпляра создаёт новый
InstanceId, переназначает внутренние ссылки и сохраняет внешние. - Reparent в v1 разрешён внутри одного экземпляра; циклы и перенос через границу вложенного экземпляра отклоняются. Перемещение экземпляра целиком допустимо. Команда явно выбирает сохранение local или world transform.
- Удаление унаследованного объекта записывает suppression объекта и его разрешённого поддерева; шаблон остаётся прежним. Локально добавленный объект удаляется из документа-владельца. Ссылки и другие patches на подавленные цели требуют разрешения конфликта, а не молчаливого удаления.
- Новая версия шаблона обновляет поля без overrides. Совместимые явные overrides сохраняются. Исчезнувшая цель, несовместимый тип, цикл и повреждённая обязательная ссылка дают конфликт; активной остаётся последняя согласованная версия до исправления.
- Структурный batch проверяет revision, строит кандидат, валидирует итоговые связи и публикует один результат с одним Undo.
Revertудаляет выбранный override и возвращает нижележащее значение; UI и MCP используют один authoring contract. - В MVP входят обычные и ациклично вложенные экземпляры, overrides полей, добавление объектов/компонентов, suppression и ограниченный reparent. Variant inheritance,
Apply overrides to template, перенос через границы экземпляров и поэлементное слияние массивов — после MVP.
Inspector показывает источник и локальные отличия, переход к шаблону, Revert и конкретные конфликтующие targets. Импортированный шаблон использует тот же принцип разделения baseline и пользовательского слоя; детали публикации поколения — в исследовании импорта.
Два режима связи
- 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, physics introduction). Это хороший 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 и расписание систем
Минимально понятный цикл:
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). Unity также подчеркивает, что FixedUpdate может вызываться несколько раз за кадр или не вызываться между кадрами и что physics simulation имеет определенное место в loop (execution order).
Не обещать ложную determinism
Fixed timestep стабилизирует расписание, но не делает автоматически всю физику детерминированной. Godot прямо предупреждает, что физика не гарантированно детерминирована (physics introduction). Для 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). Для нового движка достаточно сначала детерминированных фаз, затем parallel scheduling.
7. ECS, объектный код и data-oriented design
ECS дает cache-friendly iteration и уменьшает связанность при больших однородных наборах. Но это не религия и не универсальный public API. Data-oriented design — выбор layout по access patterns, а не обязательное использование archetype ECS (ECS FAQ). Game Programming Patterns отдельно рассматривает Component, Event Queue, Data Locality, Game Loop и другие паттерны (catalog).
Принято для 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).
8. Asset pipeline: source не равен runtime
Pipeline должен быть видимым графом:
source file + importer settings + engine version
│
▼
importer/cache
│
typed artifact + dependencies
│
▼
cooked platform package
Godot импортирует файлы в скрытые внутренние ресурсы и предупреждает, что ResourceLoader учитывает импорт, тогда как прямой FileAccess может работать в editor, но сломаться в export (import process). Unity разделяет source asset, .meta с идентичностью и кэшируемые artifacts, которые можно регенерировать (Asset Workflow, Asset Database). O3DE называет преобразование source в optimized product assets Asset Pipeline и выполняет его Asset Processor (key concepts).
Минимальный контракт importer
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.
Тестовые уровни:
- pure math/serialization/schema migrations;
- system tests на headless world;
- asset importer golden tests;
- physics adapter tests с tolerance;
- render extraction tests без GPU;
- GPU/backend smoke tests;
- editor command/undo integration tests;
- маленькие end-to-end projects: 2D, 3D, hybrid.
Главная цель — ошибка из графического кадра должна быть трассируема обратно к asset/entity/component/command.