Files
Faset_Engine/docs/studies/08-godot-ux-source-study.md
T

118 lines
36 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.
# Godot: удобство разработки как устройство движка
**Синхронизация Faset, 18.09.2026.** Принятые решения — [ARCHITECTURE.md](../ARCHITECTURE.md), этапы до/после MVP — [PLAN.md](../../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](https://github.com/godotengine/godot), скачанный командой `git clone --depth 1` в `../../../godot`. Зафиксирован commit `9c776068d6ed23acd0c78bfe534272d1d2a3a619`, дата commit `2026-09-17T12:55:51-05:00`; [version.py:3](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/version.py#L3) сообщает **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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L305), [342](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L342). Сохранение определяется отдельной связью ownership: `_parse_node` пропускает узлы, не принадлежащие сохраняемой сцене и не являющиеся editable instance: [870](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L870). Это отличается от самого положения в дереве. `Node::add_child` проверяет наличие родителя и циклы [node.cpp:1711](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/main/node.cpp#L1711), а `set_owner` отдельно требует, чтобы owner был предком [2303](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/main/node.cpp#L2303).
Особенно полезна семантика локальных ресурсов. `SceneState::make_local_resource` возвращает обычный ресурс без копирования, а для `local_to_scene` ищет уже созданную копию в remap текущей сцены и только затем дублирует: [packed_scene.cpp:781](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L781). Поэтому две ссылки внутри экземпляра могут указывать на одну локальную копию, тогда как другой экземпляр получает другую. Рекурсивное копирование учитывает `ALWAYS_DUPLICATE`, `NEVER_DUPLICATE` и remap cache: [resource.cpp:278](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/io/resource.cpp#L278). Это существенно точнее, чем кнопка «скопировать все поля».
Официальные [Resources](https://docs.godotengine.org/en/stable/tutorials/scripting/resources.html) подтверждают общую модель: узлы используют данные ресурсов, ресурсы бывают внешними и встроенными, пользовательские ресурсы получают сериализацию и Inspector. [PackedScene](https://docs.godotengine.org/en/stable/classes/class_packedscene.html) явно связывает сохранение с `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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L1428). `_parse_node` сравнивает значение со значением по умолчанию и не сохраняет совпадающее; pinned properties обходят это исключение: [1058](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/resources/packed_scene.cpp#L1058). У «default» есть происхождение: `PropertyUtils::get_property_default_value` сначала ищет override в instantiation/inheritance stack, затем экспортированный default скрипта и, наконец, native class default: [property_utils.cpp:80](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/property_utils.cpp#L80), [152](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/property_utils.cpp#L152).
Inspector использует тот же вычислитель для revert и определения, отличается ли текущее значение: [editor_inspector.cpp:906](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L906). Клик revert отправляет обычный `emit_changed`, а не тайно меняет память отдельным способом: [1271](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L1271). Даже свёрнутая секция показывает число изменённых свойств [2463](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L2463). Таким образом, provenance данных поддерживает конкретные affordances интерфейса.
**Перенос.** Хранить базовую ссылку и sparse overrides; у каждого значения иметь понятное происхождение. Нужны «перейти к источнику», «вернуть наследуемое значение», список overrides и отдельное «зафиксировать это значение», включая совпадение с нынешним default. Для Faset MVP приняты обычные и вложенные экземпляры; прежнее предложение одного уровня prefab variation **superseded**. Variant inheritance и Apply overrides to template отложены. Patch использует instance chain/ObjectId/ComponentId/FieldId, а не имя или путь узла; точные правила — в [архитектурном исследовании](01-architecture.md).
**Чего избегать.** Нельзя вычислять revert просто из zero/default C++ конструктора: пользователь ожидает значение базы. Нельзя считать равенство чисел доказательством отсутствия намеренного override. Переименование узла, изменение структуры базы, удаление свойства и изменение типа требуют явной политики миграции. Source показывает сложность этих случаев; он не доказывает, что произвольная цепочка inheritance безошибочна.
Официальная [Scene organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/scene_organization.html) также предупреждает о связности сцен через пути и рекомендует самостоятельные под-сцены и явно передаваемые зависимости. Это хороший предел для обещаний 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L4480), фильтрует editor flags [4635](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L4635), отдаёт тип, имя, hints и usage Inspector plugins; exclusive plugin останавливает выбор следующих обработчиков [5105](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L5105). Значит, тип данных и metadata действительно определяют редактор поля.
`EditorInspector::_edit_set` создаёт action с `MERGE_ENDS`, записывает do/undo property, сохраняет связанные свойства и вызывает расширяемые undo hooks: [5703](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L5703), [5754](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L5754). В action входят refresh и notifications; затем `commit_action`: [5789](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/inspector/editor_inspector.cpp#L5789). Это помогает сохранить согласованность UI и состояния объекта.
Менеджер выбирает историю сцены, built-in resource, global или remote: [editor_undo_redo_manager.cpp:63](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/editor_undo_redo_manager.cpp#L63). Custom context позволяет явно указать принадлежность [149](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/editor_undo_redo_manager.cpp#L149). Commit ведёт saved version и инвалидирует несовместимые redo branches [246](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/editor_undo_redo_manager.cpp#L246). Официальный [EditorUndoRedoManager](https://docs.godotengine.org/en/stable/classes/class_editorundoredomanager.html) подтверждает отдельные истории и предупреждает, что автоматическое определение 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/run/editor_run_bar.cpp#L312). Затем autosave/build hook, старт debugger server и subprocess: [350](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/run/editor_run_bar.cpp#L350). `EditorRun::run` передаёт проект и `--remote-debug`: [editor_run.cpp:51](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/run/editor_run.cpp#L51). Это инструментальное выполнение сцены, не специальный режим в игровом bootstrap.
Для editor-authored edits undo notify подключён к debugger: [editor_debugger_node.cpp:245](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/debugger/editor_debugger_node.cpp#L245). `_property_changed` отправляет node-path/property/value либо resource path: [script_editor_debugger.cpp:1508](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/debugger/script_editor_debugger.cpp#L1508). Runtime `LiveEditor::_node_set_func` ищет instances редактируемой сцены и применяет изменение; root transform чужого instance специально сохраняется: [scene_debugger.cpp:871](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/debugger/scene_debugger.cpp#L871).
Remote Inspector использует другой путь: `update_remote_object` посылает runtime ObjectID [script_editor_debugger.cpp:281](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/debugger/script_editor_debugger.cpp#L281), runtime находит объект и делает `set` [scene_debugger.cpp:760](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/debugger/scene_debugger.cpp#L760). Из этих тел нельзя вывести автоматическое сохранение remote-правки в исходную сцену. [Debugger panel](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/debugger_panel.html) служит официальной сверкой общего 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2826). Добавляет недостающие defaults, вызывает importer с source и отдельным output base path: [2899](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2899). Затем пишет importer version, UID, output paths и options [2930](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L2930). Порядок options сохраняется специально для уменьшения шума diff; source/destination checksums вынесены в отдельный `.md5`: [3018](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L3018).
Generated base path находится в imported-files directory и определяется исходным путём [resource_importer.cpp:541](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/core/io/resource_importer.cpp#L541). Reimport проверяет source и destination checksum [editor_file_system.cpp:727](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/file_system/editor_file_system.cpp#L727). При редакторском move `FileSystemDock` вызывает `rename_dependencies` для owners и собирает открытые сцены для reload [filesystem_dock.cpp:1688](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/docks/filesystem_dock.cpp#L1688). Значит, UID не отменяет maintenance зависимостей и путей.
Официальный [Import process](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/import_process.html) подтверждает разделение source, настроек импорта и generated data. [Import configuration](https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/import_configuration.html) описывает настройку импортируемых 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](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/plugins/editor_plugin.cpp#L134), inspector plugins [513](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/plugins/editor_plugin.cpp#L513), importers с отложенным filesystem rescan [445](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/plugins/editor_plugin.cpp#L445), viewport input/overlay [327](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/plugins/editor_plugin.cpp#L327). Inspector поддерживает undo hooks [427](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/plugins/editor_plugin.cpp#L427). Официальные [Inspector plugins](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/inspector_plugins.html) описывают протокол выбора property editor и отправку изменений через `emit_changed`.
Диагностика тоже имеет контракт: Node вызывает пользовательский `_get_configuration_warnings`, а `update_configuration_warnings` отправляет сигнал только для редактируемой сцены [node.cpp:3564](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/scene/main/node.cpp#L3564). Scene tree собирает сообщения и добавляет warning button прямо на соответствующий item [scene_tree_editor.cpp:594](https://github.com/godotengine/godot/blob/9c776068d6ed23acd0c78bfe534272d1d2a3a619/editor/scene/scene_tree_editor.cpp#L594). Это образец объяснения ошибки в её контексте, без поиска в общем логе.
**Перенос.** Дать небольшой стабильный 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](https://docs.godotengine.org/en/latest/tutorials/plugins/running_code_in_the_editor.html) отдельно предупреждает: tool/EditorScript изменения сами по себе не гарантируют Undo и dirty marking. Это ограничение надо учитывать при заимствовании, а не считать `@tool` бесплатной безопасной магией.
**Прототип и приёмка.** Расширение spawner работает без пересборки ядра, валидирует отсутствие маршрута, редактирует параметры custom widget и рисует radius. Enable/disable дважды не оставляет widgets/callbacks; исправление warning отменяется; исключение пользовательского инструмента не повреждает сцену; сцена сохраняется через общий слой, без своего serializer.
## Порядок заимствования
1. Typed properties/resources, document identity и полноценные editor transactions.
2. Scene/prefab composition, явные sharing/local semantics, provenance и revert.
3. Отдельный Player со snapshot сцены и надёжный Stop/build/Play; remote/live workflow выше оставлен сравнением Godot.
4. Source/recipe/cache import, stable asset references и reference-aware move.
5. 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 не гарантируется.