Godot 4.7 project for authoring, previewing, validating, and building Exchanger PCK themes. Includes the example and space-pixel themes, binding contracts, editor tooling, validation/build scripts, and project documentation.
22 KiB
Композитные анимации питомца: гайд для дизайнера
Композитное действие — это не один клип, а сценарий из нескольких анимаций, визуальных преобразований и перемещений. Сценарий хранится в теме, поэтому каждый питомец может вести себя по-своему, а Exchanger только предоставляет безопасные операции для экранной позиции, границ и опор.
Рабочий пример находится в сцене
interactive_pet.tscn и скрипте
interactive_pet.gd темы
space-pixel. Нейтральный API полностью описан в SCRIPTING.md, а разметка опор —
в BINDING_CONTRACT.md.
Что делает дизайнер, а что делает приложение
Тема определяет:
- набор и имена кадровых анимаций;
- порядок клипов и число повторов;
- поворот персонажа и изменение масштаба его визуальной части;
- момент отключения обычной навигации, контактов с опорами, границ и пользовательского ввода;
- перемещение экранной точки контакта и выбор момента посадки;
- восстановление визуального состояния после завершения или отмены.
Exchanger определяет:
- уникальный
run_idкаждого запуска; - фактическую экранную точку контакта питомца;
- границы текущего экрана и доступные опоры;
- применение политик движения;
- отмену устаревшего запуска при смене экрана, drag, выгрузке темы или конфликтующем действии.
Композитное действие не получает доступ к внутренним узлам Exchanger. Оно работает только со
своей сценой и переданным объектом runtime.
1. Подготовьте отдельные клипы
Сначала каждый этап должен корректно проигрываться отдельно во вкладке PET. Для действия
инопланетянина space-pixel используются:
| Этап | Клип | Режим |
|---|---|---|
| Поворот из левой ориентации | turn_left |
один цикл |
| Поворот из правой ориентации | turn_right |
один цикл |
| Приближение | come_closer |
один цикл |
| Основное действие | action |
повторяемый |
| Падение | falling |
повторяемый |
| Приземление | landing |
один цикл |
| Ожидание | idle |
зацикленный |
Имена клипов принадлежат теме и могут отличаться. Приложение видит публичное действие
interact, а тему интересует, из каких собственных клипов оно состоит.
Те же прямые анимации landing и idle используются host и вне composite-сценария: после
обычного falling при отпускании питомца или смене экрана host проигрывает одноразовый landing,
затем включает idle. Запрос celebrate, поступивший во время такого приземления, выполняется
после завершения landing.
Перед сборкой сценария проверьте:
- все ячейки sprite-sheet имеют одинаковый размер;
- точка контакта лап находится в одном месте каждого кадра;
- прозрачные поля не меняют видимый масштаб персонажа между кадрами;
- one-shot клипы не зациклены, а повторяемые клипы могут корректно начинаться заново;
get_pet_animation_frame_count()иget_pet_animation_frames_per_second()возвращают реальные значения: по ним вычисляется длительность цикла;- все текстуры импортированы с nearest-фильтрацией, если тема использует pixel art.
2. Отделите экранную позицию от визуального масштаба
Рекомендуемая структура сцены:
InteractivePet (Node2D, позиционируется приложением)
└── VisualRoot (Node2D, pivot в точке контакта с опорой)
└── Sprite (AnimatedSprite2D или другой визуальный узел)
Root InteractivePet не масштабируется композитным сценарием: его положение связано с экранной
точкой контакта. Меняйте VisualRoot.scale, чтобы приближение не сдвигало лапы с опоры. В
space-pixel VisualRoot.position совпадает с ground_offset, то есть масштабирование происходит
вокруг точки под ногами персонажа.
После успешного завершения и любой отмены возвращайте VisualRoot.scale = Vector2.ONE. Если тема
вводит дополнительные эффекты, таким же образом восстановите rotation, modulate, material и
временную видимость дочерних узлов.
3. Подключите питомца к manifest
В themes/<id>/package/manifest.json укажите production-сцену:
"InteractivePetScene": "res://exchanger_theme_dlc/components/interactive_pet.tscn"
Путь должен быть каноническим и находиться внутри res://exchanger_theme_dlc/. Сама сцена, скрипт,
sprite-sheet, звук и другие зависимости должны лежать внутри themes/<id>/package/.
4. Реализуйте базовый animation API
Композитный контракт расширяет обычного питомца, поэтому сначала должны работать базовые методы и сигналы:
signal pet_animation_started(animation_name: StringName)
signal pet_animation_completed(animation_name: StringName)
func play_pet_animation(animation_name: StringName) -> bool:
return false
func pause_pet_animation() -> void:
pass
func stop_pet_animation() -> void:
pass
func get_pet_animation_names() -> Array[StringName]:
return []
func get_pet_animation_frame_count(animation_name: StringName) -> int:
return 0
func has_pet_animation(animation_name: StringName) -> bool:
return false
func get_current_pet_animation() -> StringName:
return &""
func get_current_pet_frame() -> int:
return 0
Практически всегда также нужны
get_pet_animation_frames_per_second() и get_pet_ground_offset(). Имена анимаций, которые
host запрашивает, совпадают с именами, возвращаемыми get_pet_animation_names(). Полный список
и допустимые возвращаемые значения приведены в
SCRIPTING.md.
5. Добавьте композитный API целиком
Нельзя реализовать только часть интерфейса. Валидатор требует сразу два сигнала и три метода:
signal pet_composite_action_started(action_name: StringName, run_id: int)
signal pet_composite_action_finished(
action_name: StringName,
run_id: int,
completed: bool
)
func has_pet_composite_action(action_name: StringName) -> bool:
return action_name == &"interact"
func start_pet_composite_action(
action_name: StringName,
runtime: Object,
parameters: Dictionary
) -> int:
# Проверка, принятие запуска и запуск корутины темы.
return 0
func cancel_pet_composite_action(run_id: int, reason: StringName) -> void:
# Остановка только того запуска, которому принадлежит run_id.
pass
При принятии запуска start_pet_composite_action() обязан вернуть ровно
int(runtime.call(&"GetRunId")); при отказе — 0. После принятия испустите started, сохраните
run_id и runtime, затем запустите корутину через call_deferred(). Так метод старта не блокирует
Exchanger.
Минимальное состояние одного активного запуска:
var _composite_run_id := 0
var _composite_action := &""
var _composite_runtime: Object
func _is_composite_active(run_id: int, runtime: Object) -> bool:
return (
run_id == _composite_run_id
and runtime == _composite_runtime
and is_instance_valid(runtime)
and bool(runtime.call(&"IsActive"))
)
Проверяйте _is_composite_active() сразу после каждого await. Старая корутина не должна менять
кадр, scale или экранную позицию после отзыва runtime.
6. Составьте раскадровку действия
До написания кода удобно заполнить таблицу сценария. Для space-pixel/interact она выглядит так:
| Шаг | Что происходит | Motion policy |
|---|---|---|
| 1 | Обычная навигация и ввод приостанавливаются | suspended / ignore / allow / false |
| 2 | По предыдущей ориентации проигрывается turn_left или turn_right |
без изменения |
| 3 | come_closer, одновременно VisualRoot растёт от ×1 до ×2 |
без изменения |
| 4 | action повторяется action_repetitions раз |
без изменения |
| 5 | Выбирается самая высокая доступная опора и вычисляется длина падения | без изменения |
| 6 | falling идёт вниз, персонаж один раз проходит через низ и появляется сверху |
theme_driven / after_vertical_wrap / wrap_vertical_once / false |
| 7 | После wrap разрешается посадка только на выбранную опору | та же политика |
| 8 | Scale возвращается к ×1, проигрываются landing, затем idle |
завершение запуска |
Поля policy записываются в порядке navigation / support_contacts / screen_bounds / input_enabled. Вызов выглядит так:
Для синхронного приближения доведите VisualRoot.scale до конечного значения уже к началу
последнего кадра come_closer. Последний кадр должен быть показан хотя бы один render-frame в
полном масштабе, и только после этого можно запускать action. Если интерполировать scale до
самого конца общей длительности клипа и сразу переключить анимацию, финальный масштаб формально
будет вычислен, но пользователь не увидит его до начала следующего клипа.
var accepted := bool(runtime.call(&"SetMotionPolicy", {
"navigation": "suspended",
"support_contacts": "ignore",
"screen_bounds": "allow",
"input_enabled": false,
}))
Для падения space-pixel дополнительно передаёт landing_surface_id выбранной опоры и
wrap_margin. Все допустимые значения перечислены в
SCRIPTING.md.
7. Управляйте движением через anchor
runtime.GetAnchor() возвращает экранную точку контакта, а не позицию root сцены. Для движения
используйте MoveAnchor(delta), а не меняйте position питомца напрямую. Основные операции:
| Вызов | Назначение |
|---|---|
GetViewportRect() |
получить границы экрана |
GetAnchor() |
получить текущую точку контакта |
GetHighestSupport() |
выбрать верхнюю доступную опору |
SetMotionPolicy(policy) |
временно изменить навигацию, контакты, границы и ввод |
MoveAnchor(delta) |
переместить точку контакта с применением policy |
SetAnchor(anchor) |
установить точку явно, например выровнять X после wrap |
LandOnSupport(surface_id) |
завершить движение точной посадкой на существующую опору |
Каждый изменяющий вызов возвращает результат. Проверяйте accepted; у MoveAnchor() также читайте
wrapped, landed, surface_id и фактический anchor. Не считайте, что запрошенное перемещение
обязательно было применено без изменений.
Для равномерного перемещения вызывайте MoveAnchor() после каждого process_frame и умножайте
скорость на delta. Количество повторов падения можно вычислить так:
полная длина = путь от текущего anchor до низа + путь от верха до выбранной опоры
число циклов = ceil(полная длина / расстояние_за_один_цикл)
время = число циклов × кадры_клипа / fps
скорость = полная длина / время
falling_distance_per_cycle — художественный параметр темы: он задаёт, насколько быстро один
визуальный цикл должен продвигать персонажа по экрану.
Для этапа приближения эталонный питомец предоставляет ещё три параметра темы:
approach_visual_scale— итоговый масштаб (2.0для инопланетянина);approach_scale_completion_ratio— доля длительностиcome_closer, за которую масштаб должен полностью достигнуть целевого значения; значение0.75оставляет заключительную четверть клипа уже в полном масштабе;approach_hold_seconds— короткая выдержка полного масштаба после завершенияcome_closerи до первого кадраaction; по умолчанию0.15секунды.
Не доводите масштаб до цели только на последнем кадре: формально размер успеет измениться, но
переход визуально сольётся с началом action. Последние кадры приближения должны явно проиграться
при окончательном масштабе, а следующий клип — начаться только после выдержки.
8. Учитывайте параметры запуска
Для interact сейчас используются два параметра:
action_repetitions— число повторов основного действия; ограничьте его разумным диапазоном, например1..16;reduced_effects— режим уменьшенных эффектов. Вspace-pixelон пропускает приближение и проход через границу, но сохраняетaction,landingи возврат вidle.
Не требуйте наличия ключа: используйте parameters.get("имя", значение_по_умолчанию). Неизвестные
поля игнорируйте, чтобы новые версии приложения не ломали старую тему.
9. Корректно завершайте и отменяйте действие
Успешное завершение:
- Убедитесь, что run всё ещё активен.
- Верните временные визуальные изменения.
- Перейдите в стабильный клип, обычно
idle. - Очистите сохранённые
run_idи runtime. - Испустите
pet_composite_action_finished(action_name, run_id, true).
При ошибке собственной последовательности испустите finished(..., false). При вызове
cancel_pet_composite_action() немедленно очистите состояние и визуальные изменения; повторно
обращаться к runtime после отмены нельзя. Эталонная реализация намеренно не испускает второй
finished, если отмена уже инициирована host: владельцем окончательного lifecycle остаётся один
активный запуск.
10. Разметьте опоры экранов
Если manifest содержит InteractivePetScene, текущий Builder требует navigation v1 во всех восьми
production-сценах, кроме screen.settings. На каждом экране должны быть:
- ровно один
Node2Dсexchanger_pet_navigation_root = true; - положительные
exchanger_pet_roaming_boundsи существующийdefault_surface_id; - хотя бы одна горизонтальная поверхность;
- при необходимости — sit points и двунаправленные climb edges.
Это metadata навигации, а не exchanger_binding_id. Подробный список ключей и правила координат
приведены в BINDING_CONTRACT.md.
11. Проверьте результат
- Активируйте тему:
./activate-theme.ps1 -ThemeId <id>. - Запустите preview и откройте вкладку
PET. - Проверьте каждый клип отдельно, включая первый и последний кадр.
- Запустите «СОСТАВНОЕ ДЕЙСТВИЕ: INTERACT» несколько раз подряд.
- Отмените действие через
Esc, переход в галерею и повторный запуск. - Проверьте обычный и
reduced_effectsсценарии в Exchanger. - Выполните:
./tools/Test-PackagedBindingContract.ps1
./test-theme.ps1 -ThemeId <id>
./build-theme.ps1 -ThemeId <id>
Готовое действие не дёргает root при смене клипов, не оставляет увеличенный VisualRoot, не
реагирует на ввод во время запрещённого этапа, не цепляется за промежуточные опоры и после
приземления стабильно возвращается в idle.
Частые ошибки
- Валидатор сообщает о partial composite API. Добавлена только часть из двух сигналов и трёх методов — реализуйте весь набор или удалите его целиком.
- Персонаж смещается при увеличении. Pivot
VisualRootрасположен не в точке контакта лап. - Старая анимация продолжает работать после перехода. После
awaitне провереныrun_idиruntime.IsActive(). - Питомец цепляется за окна во время падения. Слишком рано включены
solid-контакты; для сценария с проходом через экран используйтеafter_vertical_wrap. - Питомец исчезает и не возвращается. Не проверены
wrapped/landed, неверенlanding_surface_idили поверхность лежит вне roaming bounds. - Действие имеет неверную скорость. Число кадров или FPS в API не совпадает с фактическим
SpriteFrames. - В PCK нет скрипта или текстуры. Зависимость лежит в
source/, а не внутриpackage/.