29 KiB
Faset Engine — архитектура
Редакция 1.1 · 18 сентября 2026 · принятый проект, реализация MVP в процессе.
Этот файл фиксирует решения пользователя. Принято означает выбранное направление реализации, а не автоматически завершённую возможность движка. Работающий код, выполненные проверки и текущие ограничения перечислены в журнале реализации. Этапы до MVP и после него, зависимости работ и критерии готовности находятся в PLAN.md.
Исследовательская база: Unreal Engine, Godot, Unity и Blender. Исторические сравнения не отменяют принятые здесь решения. Подробности: ECS, MCP и Blender, стек и экспорт, renderer, метаданные, импорт, build/cook.
1. Назначение и границы — принято
Faset Engine предназначен для десктопных 2D- и 3D-игр под Linux и Windows. Важны удобное ручное редактирование, полноценная работа с проектом через MCP, передовая графика, интеграция с обычным Blender и экспорт самостоятельной игры. Независимость означает собственные форматы и API на границах движка, контролируемые зависимости и отсутствие обязательных облачных сервисов; это не обещание реализации всех библиотек с нуля или отсутствия платформенных SDK.
Основной язык движка — английский: интерфейс редактора, встроенные команды, диагностические сообщения, идентификаторы публичного API и описания CLI/MCP tools. Пользовательский текст и содержимое проектов поддерживают Unicode; язык создаваемых игр не ограничивается английским. Локализация редактора относится к развитию после MVP. Текущие исследования и проектная документация могут оставаться на русском; публичный корневой README ведётся на английском.
MCP существует только в Editor и его headless-сервисах. Он работает с авторскими документами, импортом, сборкой, запуском/остановкой Play и журналами редактора. MCP не читает и не изменяет runtime world, entities, компоненты или physics state. В Player, экспортной игре и SchemaExporter нет MCP-сервера, адаптера, зависимостей или транзитивного обхода этой границы через Editor.
2. Принятый стек
- C++ — ядро и первый способ написания игровой логики; Lua обязательно добавляется следующим языковым этапом отдельным модулем. Использование Lua конкретной игрой необязательно.
- EnTT — runtime ECS за API Faset. Свои: сцены, метаданные, lifecycle, команды, планирование систем и связь с инструментами.
- Box2D / Box3D — отдельные физические миры 2D/3D; Box3D — erincatto/box3d.
- Собственный retained C++ UI — основной редактор: сохраняемое дерево виджетов, декларативные layout/styles, сначала тёмная тема. Dear ImGui — диагностические инструменты, не основная оболочка редактора.
- Vulkan 1.3, собственные renderer и RenderGraph; Slang для шейдеров и совместимого с ним подмножества HLSL.
- SDL3 за Faset Platform API — окна, события и платформенные службы; типы SDL не становятся обязательными типами gameplay API.
- CMake + Ninja, Clang на Linux, clang-cl на Windows с согласованными Windows SDK и runtime-библиотеками.
- JSON authoring, версионированные бинарные cooked-форматы, стабильные идентификаторы данных.
Выбор библиотеки не фиксирует её версию. Точные releases/commits, минимальные версии ОС/драйверов, Linux runtime baseline и разрешённые GPU formats/limits закрепляются в dependency lock и профилях перед интеграцией. Vulkan 1.3 сам по себе не гарантирует RT, mesh shaders или любые дополнительные features.
3. Процессы, модули и версии — принято
Editor владеет AuthoringService, UI, asset/build services, editor plugins и MCP adapter. Его headless-режим предоставляет те же авторские операции без окон. Player — отдельный процесс с runtime, renderer, физикой и игровым кодом; сбой gameplay не должен завершать редакторскую сессию. Это не изолирует native-плагин, загруженный непосредственно в Editor.
Gameplay оформляется отдельной статической библиотекой, связанной с Player, в development и release. Исправление .cpp пересобирает затронутые файлы и перелинковывает Player; изменения общих заголовков учитываются по графу зависимостей. Цикл разработки: stop → build → restart Play. Горячая замена C++, перенос действующих vtables/layout и сохранение произвольного runtime-состояния в этот контракт не входят.
Editor plugins — DLL/SO под конкретный Editor SDK/build, загружаемые при запуске. Обновление требует перезапуска Editor. Manifest содержит module ID/version, зависимости, назначение runtime/editor, API version и build fingerprint: ОС, архитектуру, toolchain, стандартную библиотеку/CRT и существенные настройки. Зависимости проверяются на циклы; editor и MCP targets не попадают в Player. Статическая gameplay-библиотека не означает статической линковки всех системных зависимостей.
Entry function плагина согласует версию интерфейса; память освобождает создавшая её сторона, ошибки имеют явный контракт. Нативные плагины не получают обещания стабильного ABI между любыми компиляторами. EnTT registry и детали storage не экспортируются как универсальный plugin ABI. Панели custom Inspector принадлежат отдельному Editor module и изменяют документы через общий command API.
4. Метаданные и схема без запуска gameplay в Editor — принято
Используется собственная явная регистрация C++ выбранных компонентов, полей, методов и событий. Типизированные helpers проверяют поддерживаемые accessors и сигнатуры. Универсальный parser/binding generator произвольного C++ не нужен для первой версии.
После сборки отдельный служебный SchemaExporter выполняет registration entrypoints и пишет декларативный schema manifest. Он связывается с теми же регистрациями gameplay, но не запускает мир, renderer, OnStart или игровой цикл. Editor получает TypeId, FieldId, version, переносимые value types, defaults, units, constraints и UI hints без загрузки игрового native-кода в процесс редактора. MCP в SchemaExporter отсутствует. Manifest связывается с build fingerprint; сбой/ошибка экспорта не заменяет последнюю корректную схему.
В exporter всё же исполняется C++ регистрация: отдельный процесс изолирует её сбой, но не является sandbox для недоверенного кода. Defaults объявляются явно, а не извлекаются созданием произвольного игрового мира. Декларативные ограничения валидируются AuthoringService; дополнительные исполняемые authoring-валидаторы требуют явного editor/helper-контракта, а не неявного вызова gameplay из Inspector.
TypeId/FieldId описывают смысл сохраняемых данных и не зависят от имени C++, offset, порядка членов или typeid. Layout fingerprint нужен для совместимости конкретной сборки. Переименование сохраняет ID; изменение смысла/единиц требует миграции. Отсутствующий плагин не уничтожает неизвестные записи. Lua позже использует явно экспортированный runtime API и проверяемые handles; наличие metadata не делает каждый native-метод доступным MCP или Lua.
MVP доставляет декларативные шаги миграции через тот же schema manifest: default, scale и require_manual. Вся схема и правила проверяются до публикации Player/schema generation. Старые версии открываются как opaque data; миграция выполняется явно командой component.migrate с revision, одним Undo и записью recovery. Inherited component изменяется в исходной сцене; sparse overrides других экземпляров требуют явного обзора при изменении смысла или единиц. Произвольный C++ migration callback в Editor не загружается. Контракт и проверяемый пример — в Manual.
5. Авторские данные, сцены и редактор — принято
Пользователь работает со сценами, объектами, компонентами и ресурсами. Сцены допускают вложенные экземпляры шаблонов. JSON хранит ссылку на исходный шаблон и sparse overrides — только локальные отличия, с устойчивыми адресами полей/объектов. Подписи и пути не являются идентичностью. Для добавления, удаления и изменения вложенного содержимого нужны явные операции; потерянная цель override становится диагностируемым конфликтом, а не молчаливым удалением данных.
AuthoringService владеет документами, схемами, validation, revisions, транзакциями и Undo. GUI, CLI, MCP и editor extensions вызывают одни authoring-команды. Команда получает явные document/entity/asset IDs. Ручной drag создаёт preview и один commit; MCP batch проверяется целиком и создаёт один шаг Undo. В первом контракте атомарность ограничена одним документом; конфликт revision не применяет половину изменений. Idempotency действует для одинакового key/payload в пределах объявленного scope и срока хранения журнала.
Scene compiler переводит авторские данные в runtime и сохраняет соответствие SceneEntityId → RuntimeEntity[]. Persistent SceneEntityId/AssetId отличаются от runtime handle с world/session/generation. Runtime storage не становится источником истины для JSON и не обязан повторять layout документа. Изменение игрового компонента не создаёт редакторский Undo и не сохраняется в authoring автоматически.
Retained UI получает состояние и уведомления от AuthoringService; состояние виджетов не заменяет состояние документа. Стили/layout отделены от команд; ключевые действия доступны из интерфейса и через editor API. Импорт, компиляция и cook не блокируют event loop. Необходимые текстовый ввод, focus, clipboard, DPI и платформенные поведения проверяются на обеих ОС как часть UI, а не считаются автоматически решёнными SDL3.
UI-референс не является источником истины. Сгенерированные изображения задают визуальное направление: тёмные поверхности, плотность, типографику и иерархию. Состав функций, состояние документов и поведение определяют эти контракты, PLAN и проверенные пользовательские сценарии. Референс можно менять под рабочий функционал.
6. Runtime ECS, поведение и игровой цикл — принято
EnTT используется для компонентов и запросов за типизированным API Faset. Простое поведение и массовая система обращаются к одному состоянию. Собственная ECS не разрабатывается на первом этапе; выбор EnTT не обещает большей скорости или дешёвой замены backend. Системы объявляют фазу, чтение/запись компонентов и внешних ресурсов, зависимости и диагностическое имя.
Публичный lifecycle: OnStart, FixedUpdate, Update, LateUpdate, OnDestroy. OnStart вызывается один раз после создания и привязки компонентов, перед первым обновлением; OnDestroy — один раз при применении удаления, пока допустимые данные ещё доступны. После него подписки снимаются, handles инвалидируются. Начальная загрузка проходит тот же явный барьер инициализации.
Главный цикл собирает ввод, выполняет нужное число фиксированных ticks, вызывает Update, вычисляет интерполированные presentation transforms, затем вызывает LateUpdate для камеры и зависимых визуальных объектов, финализирует render snapshot и рисует кадр. Одноразовые события ввода потребляются один раз соответствующим tick, удерживаемое состояние доступно последующим ticks. Update/LateUpdate не переписывают рассчитанный physics transform динамического тела; движения физических тел задаются командами следующего шага.
Fixed tick — 60 Гц по умолчанию, настраиваемый для проекта. Порядок tick: применить ожидающие structural commands → выполнить FixedUpdate и системы до физики → применить команды физическим телам → завершить Box2D/Box3D step → перенести transforms и события → выполнить системы реакции после физики. События доставляются через явную очередь; произвольный gameplay/Lua не вызывается из потоков physics callbacks.
Spawn/despawn и add/remove компонентов записываются в runtime structural command buffer и вступают в силу в начале следующего фиксированного tick, после завершения прежних задач. У применения один владелец; очередь имеет заданный порядок. Ссылки на память компонентов не переживают этот барьер. create/add/delete, повторное удаление и порядок lifecycle обрабатываются по единому контракту, который проверяется отдельными сценариями.
Catch-up ограничен числом ticks за проход главного цикла; начальный профиль — максимум четыре, с возможностью настройки. При перегрузке локальный Player отбрасывает избыточные целые интервалы накопленного времени, сохраняет дробный остаток и показывает dropped_time; не увеличивает physics dt произвольно. После паузы накопитель сбрасывается. Это политика локальной игры, не обещание deterministic lockstep/rollback.
Подготовка presentation transforms перед LateUpdate интерполирует предыдущий и текущий завершённые ticks с alpha = accumulator / fixed_dt; результат не записывается обратно в физику. Такой путь добавляет до одного tick визуальной задержки. Spawn, teleport и новая сессия сбрасывают историю. Motion vectors используют предыдущий отрисованный transform, а не просто предыдущий physics tick.
Первый scheduler — последовательные именованные фазы с измерением времени. Затем распараллеливаются отдельные независимые проходы: storage создаётся заранее, read/write conflicts и shared resources явно учитываются, structural changes остаются за барьером. EnTT не делает registry и пользовательские данные автоматически thread-safe. Полный task graph и подключение physics task callbacks к согласованному пулу workers вводятся после корректного последовательного варианта и профилирования.
7. Физика и renderer — принято
2D и 3D имеют отдельные миры и spatial-типы. Для динамического тела итоговым transform владеет физика; teleport и кинематическое управление — отдельные операции. Физическое тело связано с entity через проверяемый handle. Миры 2D и 3D не сталкиваются автоматически. Внутренние substeps solver настраиваются отдельно от частоты gameplay ticks; начальная настройка — четыре substeps с проверкой на сценах проекта.
Собственный Vulkan 1.3 renderer получает подготовленный snapshot, не обходит изменяемый EnTT registry с render thread. RenderGraph описывает reads/writes ресурсов, порядок проходов, barriers и lifetimes. Первым нужен проверяемый raster-путь 2D/3D без обязательных RT/mesh shaders; GPU-driven visibility и другие передовые механизмы добавляются по измеримым этапам из плана.
Vulkan API вызывается напрямую внутри собственного backend Faset. Он владеет Vulkan handles, созданием GPU-ресурсов и pipelines, записью команд, синхронизацией и отправкой в очереди. Renderer и RenderGraph используют небольшой внутренний интерфейс ресурсов и команд Faset; Vulkan-типы и вызовы vk* не входят в gameplay API или команды редактора. SDL3 обеспечивает окно и создание Vulkan surface, но не заменяет графический backend; Slang отвечает за компиляцию шейдеров. Универсальная абстракция нескольких графических API не является задачей MVP.
Slang компилирует шейдеры в SPIR-V и выдаёт сведения для согласования CPU/GPU данных. Совместимый HLSL проходит выбранный pipeline; поддержка любого существующего HLSL-кода не обещается. Cook учитывает compiler/version, includes, defines и GPU profile. Nanite/Lumen-подобные системы остаются исследовательскими направлениями, не готовыми возможностями MVP.
8. Blender и ассеты — принято
Используется обычный Blender, без обязательного форка или установленного MCP-плагина. Базовый обмен — GLB и manifest со стабильными asset/subasset IDs. Обычный экспорт принимается движком; удобный add-on для публикации, UUID и повторного экспорта остаётся необязательным помощником. Если источник не предоставляет устойчивые subasset IDs, importer не обещает надёжно угадать соответствие после произвольного rename/reparent: сохраняет mapping и показывает конфликты.
.blend и исходные изображения отделены от cooked/cache-данных. Геометрия и skeleton принадлежат источнику, placement/gameplay — сцене Faset, локальные замены материалов — overrides. Reimport сохраняет ссылки и локальные отличия; удалённые или конфликтующие outputs видны пользователю. Export recipe фиксирует units/axes, материалы и поддержанный animation profile. Blender nodes не превращаются автоматически в Slang shaders. Live link и расширенный roundtrip следуют после надёжного файлового импорта по плану.
9. Build, доставка и проверка — принято
Один сервис сборки для Editor/CLI/editor MCP разделяет компиляцию C++, schema export, компиляцию шейдеров, cook и упаковку. CMake/Ninja отвечают за native build graph. Inputs фиксируются snapshot, зависимости явные, worker пишет в свой build tree; cache учитывает content, recipe, toolchain и версии форматов. Candidate публикуется лишь после проверки, предыдущая успешная сборка остаётся доступной.
Development и release Player содержат runtime, выбранные игровые модули и бинарные cooked assets. Editor UI, editor plugins, import/build services, SchemaExporter и MCP не входят в экспортную игру. Lua включается только при зависимости проекта. JSON authoring, стабильные IDs, schema versions и миграции не заменяются дампом памяти C++.
Первый путь доставки — самостоятельный каталог/архив игры и отдельно symbols. Windows и Linux собираются и проверяются собственными native workers. Clang/clang-cl не устраняют необходимость платформенных SDK/runtime; кросс-экспорт из Linux в Windows не считается готовой возможностью. Полностью побайтово воспроизводимый executable и стабильный ABI любых компиляторов не обещаются наличием build manifest.
Критерии плана включают цикл сцена → Inspector → MCP authoring edit → Undo → save → build → отдельный Player, повторный импорт GLB с сохранением overrides, lifecycle/handle tests и запуск на обеих ОС без Editor/Blender/MCP/SDK. Выполненные проверки и незакрытые условия перечислены в журнале реализации; наличие теста само по себе не означает прохождения на обеих ОС.
История решений
- 17.09.2026: выбраны имя Faset Engine, Linux/Windows desktop 2D/3D, ручная работа и MCP, интеграция с Blender, Box2D и Box3D; подготовлены исследования.
- 18.09.2026: принят порядок C++ → Lua, затем EnTT, собственный retained UI, SDL3, Vulkan 1.3/RenderGraph/Slang и CMake/Ninja/Clang.
- 18.09.2026: утверждены процессы и linkage, явная metadata registration/schema export, JSON/binary formats, вложенные шаблоны/sparse overrides, lifecycle, фиксированный tick и последовательный scheduler; граница MCP ограничена Editor/headless authoring services.
- 18.09.2026: английский принят основным языком интерфейса, диагностик и публичного API; уточнена граница прямых Vulkan-вызовов внутри собственного backend.
Реализация идёт по PLAN.md. Последующие изменения принятых контрактов фиксируются здесь с причиной и способом проверки.