36 KiB
Godot: удобство разработки как устройство движка
Синхронизация Faset, 18.09.2026. Принятые решения — ARCHITECTURE.md, этапы до/после MVP — PLAN.md. Ниже сохранено исследование чужих исходников; это не отчёт о реализованных возможностях Faset. Рекомендации, помеченные superseded, остаются только историей рассмотренных вариантов.
Для Faset приняты authoring objects/components с JSON и постоянными IDs, EnTT runtime, явная C++ metadata schema (TypeId/FieldId) и собственный retained C++ UI. Godot inheritance/live editing исследованы как сравнение. MVP Faset использует nested composition и overrides без variants/Apply to template, отдельный Player со snapshot сцены и MCP только в редакторе: authoring/import/build/PlayStop/editor logs, без чтения или изменения runtime world.
Исследован публичный репозиторий Godot, скачанный командой git clone --depth 1 в ../../../godot. Зафиксирован commit 9c776068d6ed23acd0c78bfe534272d1d2a3a619, дата commit 2026-09-17T12:55:51-05:00; version.py:3 сообщает 4.8.0 dev, а не стабильный релиз. Размер рабочего каталога с Git на момент исследования — около 429 МБ. Исходники не изменялись. Локального AGENTS.md и graphify-out/graph.json в клоне нет; применены указания локальный AGENTS.md.
Изучены тела функций в Node, Resource, SceneState/PackedScene, PropertyUtils, EditorInspector, EditorUndoRedoManager, run/debugger, filesystem/importer и EditorPlugin. Официальная документация stable сверена 17 сентября 2026 года для общих пользовательских контрактов; детали реализации относятся именно к указанному dev commit. Сборка, запуск редактора и пользовательский плейтест не проводились. Выводы о полезности и предложенные критерии — инженерная оценка, а не измерение скорости работы пользователей.
Самый ценный перенос из Godot — связанный путь от данных до действия пользователя: свойства объекта автоматически становятся редактируемыми; изменение проходит через undo, сериализацию, обновление интерфейса и при необходимости live editing. Панели удобны благодаря этому общему основанию. В Faset принят общий authoring contract, при этом runtime хранится в EnTT, UI и renderer реализуются отдельно; live editing Godot не становится runtime MCP API.
1. Сцена как повторно используемая сборка, Resource как явная модель данных
Сценарий. Дизайнер собирает дверь из визуальной части, коллизии и скрипта, сохраняет её отдельно и ставит пять экземпляров в уровень. Геометрия и общая конфигурация используются совместно; состояние открытия и специально локализованный материал принадлежат конкретному экземпляру. Такой же способ сборки должен работать для оружия, UI-панели и комнаты.
Что делает код. SceneState::instantiate создаёт вложенный PackedScene тем же механизмом, что и корневую сцену: packed_scene.cpp:305, 342. Сохранение определяется отдельной связью ownership: _parse_node пропускает узлы, не принадлежащие сохраняемой сцене и не являющиеся editable instance: 870. Это отличается от самого положения в дереве. Node::add_child проверяет наличие родителя и циклы node.cpp:1711, а set_owner отдельно требует, чтобы owner был предком 2303.
Особенно полезна семантика локальных ресурсов. SceneState::make_local_resource возвращает обычный ресурс без копирования, а для local_to_scene ищет уже созданную копию в remap текущей сцены и только затем дублирует: packed_scene.cpp:781. Поэтому две ссылки внутри экземпляра могут указывать на одну локальную копию, тогда как другой экземпляр получает другую. Рекурсивное копирование учитывает ALWAYS_DUPLICATE, NEVER_DUPLICATE и remap cache: resource.cpp:278. Это существенно точнее, чем кнопка «скопировать все поля».
Официальные Resources подтверждают общую модель: узлы используют данные ресурсов, ресурсы бывают внешними и встроенными, пользовательские ресурсы получают сериализацию и Inspector. PackedScene явно связывает сохранение с owner.
Перенос. Нужны authoring scene/prefab, typed asset references и три понятных режима данных: общий asset, локальное состояние экземпляра, явно созданная уникальная копия. В UI полезнее показывать «изменение затронет все экземпляры», имя источника и переход к нему, чем рассчитывать на знание ref-counting. Внутреннее дерево редактора можно компилировать в ECS; исходники Godot не обязывают переносить его runtime object model.
Чего избегать. Не делать ownership невидимым условием сохранения для обычного автора: узел, видимый в редакторе, но не попадающий в файл, создаёт неприятный сюрприз. Не смешивать «встроен в файл» и «уникален для экземпляра» — это разные свойства. Избегать глубоко вложенных shared mutable ресурсов без индикации области воздействия.
Минимальный прототип и приёмка. Одна дверь, два references на её материал внутри сцены, пять экземпляров, external DoorConfig. Изменение config обновляет все экземпляры; local material меняется только у одной двери, сохраняя sharing её внутренних ссылок; save/reload сохраняет эти отношения. Пользователь должен объяснить область воздействия изменения, глядя на интерфейс, без чтения документации.
2. Наследование и overrides должны быть обозримыми и обратимыми
Сценарий. Есть базовый противник и тяжёлый вариант. Вариант меняет здоровье и скорость, но наследует анимации. Автор повышает базовую скорость, хочет понять, какой вариант получил изменение, и отменить только локальное переопределение здоровья.
Что делает код. При сериализации наследуемой сцены сохраняется ссылка на базовый PackedScene: packed_scene.cpp:1428. _parse_node сравнивает значение со значением по умолчанию и не сохраняет совпадающее; pinned properties обходят это исключение: 1058. У «default» есть происхождение: PropertyUtils::get_property_default_value сначала ищет override в instantiation/inheritance stack, затем экспортированный default скрипта и, наконец, native class default: property_utils.cpp:80, 152.
Inspector использует тот же вычислитель для revert и определения, отличается ли текущее значение: editor_inspector.cpp:906. Клик revert отправляет обычный emit_changed, а не тайно меняет память отдельным способом: 1271. Даже свёрнутая секция показывает число изменённых свойств 2463. Таким образом, provenance данных поддерживает конкретные affordances интерфейса.
Перенос. Хранить базовую ссылку и sparse overrides; у каждого значения иметь понятное происхождение. Нужны «перейти к источнику», «вернуть наследуемое значение», список overrides и отдельное «зафиксировать это значение», включая совпадение с нынешним default. Для Faset MVP приняты обычные и вложенные экземпляры; прежнее предложение одного уровня prefab variation superseded. Variant inheritance и Apply overrides to template отложены. Patch использует instance chain/ObjectId/ComponentId/FieldId, а не имя или путь узла; точные правила — в архитектурном исследовании.
Чего избегать. Нельзя вычислять revert просто из zero/default C++ конструктора: пользователь ожидает значение базы. Нельзя считать равенство чисел доказательством отсутствия намеренного override. Переименование узла, изменение структуры базы, удаление свойства и изменение типа требуют явной политики миграции. Source показывает сложность этих случаев; он не доказывает, что произвольная цепочка inheritance безошибочна.
Официальная Scene organization также предупреждает о связности сцен через пути и рекомендует самостоятельные под-сцены и явно передаваемые зависимости. Это хороший предел для обещаний prefab workflow.
Прототип и приёмка Faset. Шаблон → вложенный экземпляр → два размещения в сцене. Изменить default базы, добавить override, pin совпадающее значение, выполнить revert/undo, затем save/reload. Override layer должен сохранять только намеренные отличия; UI должен показывать, откуда пришло значение. При удалении target свойства нужен видимый orphaned override, а не молчаливое исчезновение пользовательского решения — это предлагаемое улучшение собственного движка.
3. Inspector и Undo — единый редакторский транзакционный слой
Сценарий. Автор тянет slider скорости, меняет несколько объектов сразу, переключается на другую сцену и нажимает Undo. Он ожидает возврата целого жеста в правильном документе и восстановления связанных свойств, а не сотни микрошагов или поломанного объекта.
Что делает код. Inspector получает property list editor_inspector.cpp:4480, фильтрует editor flags 4635, отдаёт тип, имя, hints и usage Inspector plugins; exclusive plugin останавливает выбор следующих обработчиков 5105. Значит, тип данных и metadata действительно определяют редактор поля.
EditorInspector::_edit_set создаёт action с MERGE_ENDS, записывает do/undo property, сохраняет связанные свойства и вызывает расширяемые undo hooks: 5703, 5754. В action входят refresh и notifications; затем commit_action: 5789. Это помогает сохранить согласованность UI и состояния объекта.
Менеджер выбирает историю сцены, built-in resource, global или remote: editor_undo_redo_manager.cpp:63. Custom context позволяет явно указать принадлежность 149. Commit ведёт saved version и инвалидирует несовместимые redo branches 246. Официальный EditorUndoRedoManager подтверждает отдельные истории и предупреждает, что автоматическое определение context иногда ошибается.
Перенос. Сначала сделать универсальную команду редактирования {document, object, property, before, after} с transaction boundary, merge key, dirty revision и event notifications. Drag gesture — одна команда; изменение взаимосвязанных параметров — одна транзакция. Serializer, Inspector, gizmos, bulk-edit и plugins используют одинаковые descriptor IDs. Пользовательская UI-специализация должна посылать изменения в этот слой.
Чего избегать. Не добавлять undo как последнюю оболочку вокруг готовых инструментов: побочные изменения уже будут потеряны. Не делать document context догадкой без возможности задать его явно. Нельзя копировать всю глобальную историю Godot без анализа: в этом source global edits могут очищать redo других сцен; собственному UI нужно ясно объяснять такую модель. Не считать каждый setter чистым присваиванием.
Прототип и приёмка. Две открытые сцены, общий ресурс и custom slider. 100 обновлений drag отменяются одним жестом; редактирование другой сцены не отменяет неожиданно первую; Undo восстанавливает и зависимое значение. Сохранение, Undo к saved revision, Redo и новая ветка должны правильно менять dirty indicator. Custom property widget получает это поведение без собственного Undo кода.
4. Запуск отдельной сцены и два явно различимых пути live editing
Сценарий. Разработчик проверяет экран инвентаря без прохождения главного меню, меняет расстояние взаимодействия у работающей двери и выясняет, почему runtime объект получил неожиданное значение. Важна ясность: он сейчас меняет исходную сцену или только живой экземпляр.
Что делает код. Run bar выбирает отдельную current/custom scene либо project main; для несохранённой сцены запускает save flow: editor_run_bar.cpp:312. Затем autosave/build hook, старт debugger server и subprocess: 350. EditorRun::run передаёт проект и --remote-debug: editor_run.cpp:51. Это инструментальное выполнение сцены, не специальный режим в игровом bootstrap.
Для editor-authored edits undo notify подключён к debugger: editor_debugger_node.cpp:245. _property_changed отправляет node-path/property/value либо resource path: script_editor_debugger.cpp:1508. Runtime LiveEditor::_node_set_func ищет instances редактируемой сцены и применяет изменение; root transform чужого instance специально сохраняется: scene_debugger.cpp:871.
Remote Inspector использует другой путь: update_remote_object посылает runtime ObjectID script_editor_debugger.cpp:281, runtime находит объект и делает set scene_debugger.cpp:760. Из этих тел нельзя вывести автоматическое сохранение remote-правки в исходную сцену. Debugger panel служит официальной сверкой общего debug workflow; конкретное различие путей подтверждено локальным source.
Принятый перенос. Separate Player process и запуск snapshot текущей сцены либо project entry point. Сохраняется authoring context; Stop → incremental C++ build → Play создаёт новый snapshot. Superseded для MVP: remote tree/property edits, пересылка authoring transactions в runtime и Apply runtime value в источник. MCP управляет запуском на стороне редактора, но не инспектирует и не меняет runtime world; в Player и экспорте MCP отсутствует. Маленький test context позволяет проверить сцену без прохождения всей игры.
Чего избегать. Не обещать произвольный state-preserving hot reload на основании property live edit. В показанном коде ref-counted resources без пути не пересылаются этим способом; структура сцены и instance mapping тоже накладывают ограничения. Runtime deletion, повторно использованные IDs, invalid paths и разрыв соединения должны быть нормальными состояниями protocol.
Прототип и приёмка Faset. Запустить дверную сцену отдельным Player, изменить исходный документ и Undo во время игры: текущий snapshot остаётся прежним. После Stop/Play запускается новая версия; runtime изменения не записаны в source. Проверить два экземпляра, сбой Player, повторный запуск и сохранение редакторского контекста. У MCP отсутствуют runtime query/mutation tools, у Player — MCP endpoint. Измерять время build/start отдельно, без обещания фиксированной latency.
5. Неразрушающий импорт и устойчивые ссылки — часть UX, а не только build pipeline
Сценарий. Художник меняет исходную текстуру или glTF, возвращается в редактор, получает обновление с прежними настройками. Другая машина воспроизводит импорт; перенос ассета в редакторе не требует ручного поиска всех ссылок.
Что делает код. _reimport_file читает сохранённые params, importer и UID из sidecar .import: editor_file_system.cpp:2826. Добавляет недостающие defaults, вызывает importer с source и отдельным output base path: 2899. Затем пишет importer version, UID, output paths и options 2930. Порядок options сохраняется специально для уменьшения шума diff; source/destination checksums вынесены в отдельный .md5: 3018.
Generated base path находится в imported-files directory и определяется исходным путём resource_importer.cpp:541. Reimport проверяет source и destination checksum editor_file_system.cpp:727. При редакторском move FileSystemDock вызывает rename_dependencies для owners и собирает открытые сцены для reload filesystem_dock.cpp:1688. Значит, UID не отменяет maintenance зависимостей и путей.
Официальный Import process подтверждает разделение source, настроек импорта и generated data. Import configuration описывает настройку импортируемых 3D scenes и ограничение прямого редактирования производных данных.
Перенос. Source + маленький versioned recipe + disposable derived cache. Stable asset ID, dependency index, понятный progress/status и транзакционный remap при move. User overrides хранить отдельно от регенерируемого результата. Для начала достаточно PNG и одного mesh format; remote DDC и сотни импортёров преждевременны.
Чего избегать. Не записывать ручные правки в файл, который importer завтра перезапишет. Не делать GPU cache source of truth. Не обещать полную защиту всех строковых путей одним UID: scene/script references и пользовательские данные требуют правил. Проверка checksum не означает, что importer доказанно детерминирован.
Прототип и приёмка. Изменить source, поменять import recipe, перенести asset через browser, удалить derived cache, открыть проект на чистой машине. Связи и recipe сохраняются, cache восстанавливается, Git diff отражает пользовательские решения. Ошибка importer оставляет диагностируемое состояние; для своего движка разумно дополнительно сохранять последнюю успешную версию результата до завершения нового импорта.
6. Предметные инструменты и локальная диагностика вместо обязательной правки движка
Сценарий. Команда делает редактор волн противников: специальный Inspector, dock со списком волн, gizmo радиуса появления и предупреждение у spawner без точки назначения. Всё должно участвовать в обычном Undo и исчезать корректно при выключении расширения.
Что делает код. EditorPlugin предоставляет registration/removal для docks editor_plugin.cpp:134, inspector plugins 513, importers с отложенным filesystem rescan 445, viewport input/overlay 327. Inspector поддерживает undo hooks 427. Официальные Inspector plugins описывают протокол выбора property editor и отправку изменений через emit_changed.
Диагностика тоже имеет контракт: Node вызывает пользовательский _get_configuration_warnings, а update_configuration_warnings отправляет сигнал только для редактируемой сцены node.cpp:3564. Scene tree собирает сообщения и добавляет warning button прямо на соответствующий item scene_tree_editor.cpp:594. Это образец объяснения ошибки в её контексте, без поиска в общем логе.
Перенос. Дать небольшой стабильный editor API: selection, property descriptors, commands, preview drawing, asset picker, dock lifecycle и structured validation. Для первого custom tool лучше десять согласованных точек расширения, чем доступ ко всем внутренним singleton. Вычисляемые warning должны объяснять исправление: «укажите маршрут», а не «invalid state». Кнопка исправления — отдельная undoable command.
Чего избегать. Не давать plugin обходить transaction layer ради удобства. Не исполнять автоматически тяжёлую генерацию на каждый paint/tick. User tool code внутри редактора требует аккуратных lifecycle и error boundaries. Официальное Running code in the editor отдельно предупреждает: tool/EditorScript изменения сами по себе не гарантируют Undo и dirty marking. Это ограничение надо учитывать при заимствовании, а не считать @tool бесплатной безопасной магией.
Прототип и приёмка. Расширение spawner работает без пересборки ядра, валидирует отсутствие маршрута, редактирует параметры custom widget и рисует radius. Enable/disable дважды не оставляет widgets/callbacks; исправление warning отменяется; исключение пользовательского инструмента не повреждает сцену; сцена сохраняется через общий слой, без своего serializer.
Порядок заимствования
- Typed properties/resources, document identity и полноценные editor transactions.
- Scene/prefab composition, явные sharing/local semantics, provenance и revert.
- Отдельный Player со snapshot сцены и надёжный Stop/build/Play; remote/live workflow выше оставлен сравнением Godot.
- Source/recipe/cache import, stable asset references и reference-aware move.
- Plugin API и локальная validation как проверка качества предыдущих контрактов.
Для сопоставления с Unreal и третьим движком: Godot здесь особенно ценен связью authoring model с ежедневным циклом разработки. Его node hierarchy не следует навязывать высокопроизводительному runtime, а editor transactions и provenance полезны почти независимо от renderer. Главный критерий будущего прототипа — можно ли создать, настроить, проверить, переиспользовать и исправить небольшой игровой объект без специальных обходных путей и без потери авторских изменений.
Обычный glTF/GLB import Faset работает без Blender add-on. Используется официальный Blender; optional Python add-on обеспечивает IDs и удобный экспорт, но не модифицирует движок Blender. Без устойчивых source IDs соответствие частей после rename не гарантируется.