Корневое движение спрайтов и циклы ходьбы
В этом руководстве описано, как FOnline переносит покадровое смещение 2D-спрайта через запекание изображений и использует его для согласования отображаемого цикла ходьбы или бега движущегося криттера с авторитетным перемещением по гексам. Руководство следует текущему baker, декодеру ресурсов, загрузчику спрайтов, интерполяции движения, клиентскому коду представления и тестам Engine. Встраиваемый проект отвечает за исходную графику, сопоставление анимаций, настройку движения и визуальные приёмочные тесты.
Статус контракта
Это принадлежащий Engine production-контракт для покадрового смещения 2D-спрайтов, переноса запечённых offsets, выбора кадра по движению, передачи фазы цикла и нормализации offset при остановке. Нормативными являются исходники и тесты Engine. Last Frontier и FOnline TLA служат только discovery- и compatibility-evidence; их ассеты, визуальная политика и исторические имена реализации не расширяют этот контракт.
Контракт можно использовать независимо от checkout любого из этих проектов. Project evidence закреплено в BuildTools/ExternalProjectEvidence.json, а каждое переиспользуемое утверждение на этой странице повторно выведено из текущих исходников или тестов Engine.
Это контракт представления движения для 2D. Общий импорт PNG/TGA и legacy-форматов, композиция FOFRM, запечённые записи, стандартная runtime-загрузка, атласы и кэши описаны в Форматах изображений и спрайтов. Он не зависит от скелетной анимации .fo3d и метаданных длительности из Анимации моделей.
Область и источники истины
Контрактом владеют следующие исходники:
Source/Tools/ImageBaker.*-FrameShot::NextX/NextY, импорт исходных форматов, преобразования и сериализация запечённых спрайтов;Source/Common/SpriteResource.*- общий декодер кадров SpriteResource v2 и переносNextOffset;Source/Client/DefaultSprites.*-SpriteSheet::_sprOffset, загрузка запечённых данных, общие кадры и листы по направлениям;Source/Common/Movement.*- авторитетный прогресс времени/пути и построение целочисленного пиксельногоHexOffset;Source/Common/Geometry.*- re-split без потери позиции между гексом и offset с учётом проходимости;Source/Client/CritterHexView.*- anchor цикла, displacement движения, выбор кадра, отображаемый animation offset и нормализация при остановке;Source/Client/ResourceManager.cpp- выбор анимации, извлечение кадров, merge, clone и их внутренние преобразования offsets;Source/Tests/Test_ImageBaker.cppиSource/Tests/Test_Geometry.cpp- покрытие offsets на уровне формата и нормализации геометрии.
Документация и контент проектов полезны как integration evidence, но не являются источником истины для переиспользуемого поведения Engine.
Три независимые позиции
Для движущегося 2D-криттера участвуют три связанные величины:
- Логическое движение:
MovingContextвладеет прогрессом пути, текущим гексом, направлением, завершением и скоростью движения. - Интерполяция внутри гекса:
MovingContext::BuildProgressпреобразует плавную позицию сегмента в целочисленные пиксельные значенияHexOffset.CritterHexView::ProcessMovingприменяет логический гекс и offset. - Авторское смещение спрайта: каждый кадр анимации содержит целочисленную delta
(NextX, NextY).CritterHexViewвыбирает кадр и вычисляет_offsAnim, чтобы отображаемый спрайт следовал созданному циклу ходьбы, пока логическое движение остаётся авторитетным.
Sprite root motion является данными представления. Оно не перемещает сущность, не выбирает путь, не меняет скорость, не разрешает переход между гексами и не реплицирует движение по сети.
Создание покадровых offsets
ImageBaker::FrameShot хранит:
int16_t NextX;
int16_t NextY;
Каждая пара задаёт отображаемое смещение одного кадра в пикселях. Для анимации направления из N кадров:
delta[i] = (NextX[i], NextY[i])
accum[i] = sum(delta[0..i])
T = sum(delta[0..N-1])
T - авторское смещение полного цикла. Оно может быть меньше или больше одного гекса и не обязано быть параллельно вектору одного шага сетки. Runtime проецирует движение на T и не предполагает конкретный шаг гекса или длину цикла.
Загрузчики форматов изображений получают эти значения из своих исходных записей. Полный контракт descriptor приведён в грамматике FOFRM. Для root-motion authoring текущий parser принимает имена ключей кадров и в lower-case, и в camel-case:
[dir_0]
next_x_0 = 4
next_y_0 = -2
NextX_1 = 3
NextY_1 = -1
Вложенные ссылки FOFRM добавляют offset ссылающегося кадра к выбранному вложенному кадру. Другие поддерживаемые форматы переносят или вычисляют offsets по правилам своего importer. Отсутствующие значения обычно становятся нулём; проверяйте loader конкретного формата в ImageBaker, а не предполагайте, что каждый формат предоставляет одинаковые авторские поля.
Offsets зависят от направления. Image baking может отражать, обрезать или компоновать кадры. Клиентский выбор ресурсов может извлечь первый или последний кадр, клонировать sheet либо объединить базовую и дополнительную Fallout-анимации. Этот merge path является внутренним поведением Engine и не имеет focused per-axis regression в текущем native suite; не считайте сохранение offsets на splice публичной гарантией. Проверяйте выбранный baked sheet по обеим осям и не редактируйте baked stream.
Запечённое и runtime-представление
ImageBaker::BakeCollection записывает NextX и NextY каждого конкретного кадра после преобразований как знаковые 16-битные значения в запись кадра SpriteResource v2. Запись deduplicated shared frame ссылается на более ранний кадр. ReadSpriteResource декодирует пару в SpriteResourceFrameData::NextOffset вместе с draw offset, размерами, пикселями и опциональной mesh кадра.
DefaultSpriteFactory::LoadSprite создаёт SpriteSheet для направления и копирует декодированный NextOffset каждого конкретного кадра в _sprOffset. Shared frame копирует спрайт по ссылке и его уже декодированный offset. SpriteSheet::GetSprOffset() предоставляет клиентскому runtime read-only span.
Binary layout и хранение _sprOffset являются приватными контрактами Engine. Игровые скрипты и инструменты должны создавать поддерживаемые исходные форматы и проверять отображаемое поведение, а не разбирать или изменять baked stream.
Когда root motion управляет криттером
Ветка движения в CritterHexView::Process активна, только когда выполняются все условия:
- криттер использует 2D-спрайт, а не загруженную 3D-модель;
- сейчас не активна явно поставленная в очередь action animation;
- у криттера есть активный
MovingContext; - выбранный locomotion sheet сообщает
CritterActionAnim::WalkилиCritterActionAnim::Run.
В этой ветке кадр выбирает позиция движения. Прошедшее время анимации не продвигает цикл ходьбы/бега независимо.
Для анимаций, отличных от walk/run, SetAnimSpr накапливает offsets до выбранного кадра включительно в _offsAnim. Это позволяет авторскому action визуально смещаться, пока логический криттер остаётся привязанным к месту. Sheet walk/run без активного движения не считается самостоятельной командой root motion.
Непрерывный displacement движения
EvaluateMovementDisplacement измеряет текущую целочисленную пиксельную позицию криттера относительно начала активного MovingContext:
pos = GetHexOffset(start_hex, current_hex)
+ current_hex_offset
- start_hex_offset
Логический гекс и HexOffset при пересечении границы гекса меняются в противоположных направлениях, поэтому их сумма остаётся пространственно непрерывной. Благодаря этому animation phase переживает логические snap между гексами.
MovingContext::BuildProgress вычисляет плавную интерполяцию сегмента и округляет каждый компонент HexOffset до целого пикселя. Результат намеренно не ограничивается одним гексом. Округлённый прогресс пути может отставать от плавной позиции, а client reconciliation при быстром stop/start может включить межгексовую delta, поэтому корректный движущийся offset способен охватывать больше одного гекса. Отображение остаётся правильным, потому что позиция спрайта равна сумме current_hex + offset; clamp нарушил бы этот invariant. Light fan отдельно ограничивает собственную копию в CritterHexView::RefreshOffs.
Остановка и нормализация offset
SetMoving и StopMoving очищают anchor sheet и phase displacement. Затем StopMoving вызывает CritterHexView::NormalizeHexOffset, чтобы перенести целые гексы из потенциально большого HexOffset в логический гекс, сохранив отображаемую мировую позицию.
GeometryHelper::NormalizeHexOffset заново представляет ту же пиксельную позицию как ближайший логический гекс плюс внутригексовый остаток. CritterHexView передаёт predicate, отклоняющий поля карты с флагом MoveBlocked. Клиент соблюдает следующие правила:
- если нормализация остаётся на текущем гексе, остаток можно обновить без проверки проходимости;
- если ближайший целевой гекс находится внутри карты и проходим, логический гекс и остаток обновляются, после чего offsets спрайта обновляются;
- если цель находится вне карты или заблокирована, нормализация отклоняется, а существующая пара гекс/offset остаётся без изменений;
- оба успешных представления рисуются в одной мировой позиции, поэтому обычная остановка не создаёт визуального скачка.
Не ограничивайте live movement offset, имитируя этот lifecycle step. Multi-hex offsets корректны во время интерполяции; нормализация является stop-time re-partition с проверкой карты.
Anchor и фаза цикла
CritterHexView хранит anchor для конкретного sheet:
nptr<const SpriteSheet> _walkAnchorAnim;
ipos32 _walkAnchorDisp;
Для активного sheet:
rel = pos - anchor
rel_dot_total = dot(rel, T)
total_dot_total = dot(T, T)
cycle_proj = rel_dot_total mod total_dot_total
Отрицательный результат modulo переносится в [0, total_dot_total). Поэтому cycle_proj / total_dot_total задаёт текущую phase в [0, 1) без floating-point projection.
Первый sheet нового движения получает anchor в текущем pos, то есть фазу ноль. Следующее движение получает новый anchor, потому что оба lifecycle-метода очищают предыдущий sheet и displacement.
Выбор кадра
EvaluateMovementFrameIndex проецирует каждое накопленное авторское смещение на T:
accum_dot_total[i] = dot(accum[i], T)
Выбирается первый кадр с наименьшим абсолютным расстоянием:
abs(accum_dot_total[i] - cycle_proj)
Таким образом выбирается авторская pose, ближайшая к текущей позиции криттера вдоль оси цикла. Алгоритм использует целочисленную арифметику int32_t / int64_t для displacement и dot products.
Если T равен нулю, dot(T, T) также равен нулю и функция возвращает кадр 0. Ветка offset walk/run также оставляет _offsAnim нулевым. Цикл с нулевой суммой является допустимыми fallback-данными, но не может пространственно управлять циклом ходьбы.
Отображаемый offset
Для ненулевого T метод SetAnimSpr вычисляет целый номер цикла floor division, включая отрицательный проецируемый displacement:
cycle_number = floor(rel_dot_total / total_dot_total)
cycle_start = anchor + cycle_number * T
offs_anim = (cycle_start - pos) + accum[i]
Обычная позиция спрайта на карте уже содержит логический гекс и HexOffset. Добавление offs_anim компенсирует линейное движение внутри выбранного кадра и помещает спрайт в авторскую накопленную позицию этого кадра. Изображение продвигается на следующую авторскую delta при смене выбранного кадра, пока логическое движение продолжается под ним.
Это выравнивание, а не locomotion, управляемая анимацией. Изменение NextX / NextY меняет визуальный шаг и выбор фазы, но не заставляет server-side entity двигаться дальше или быстрее.
Смена направления и sheet
Поворот внутри одного MovingContext обычно выбирает другой direction-specific SpriteSheet с иным T и, возможно, числом кадров. Перезапуск с нулевой фазы визуально сбрасывал бы походку на каждом повороте.
Когда pointer выбранного sheet меняется, Process сохраняет прежнюю обёрнутую фазу. Новый anchor сдвигается так, чтобы projection на новый cycle vector начиналась с той же доли:
old_phase = old_cycle_proj / dot(T_old, T_old)
new_anchor = pos - round(old_phase * T_new)
Компоненты вычисляются знаковым целочисленным округлением. Если прежний total vector равен нулю, сохранять нечего и новый sheet получает anchor в текущей позиции.
Логика действует для любой смены выбранного sheet walk/run, не только для изменения направления. Content substitution или resource selection, вернувшие другой sheet, проходят тот же re-anchoring path.
Ошибки и fallback-поведение
| Условие | Текущий результат |
|---|---|
| Отсутствует offset кадра FOFRM | Parser использует ноль для этого компонента. |
| При запекании offset выходит за диапазон знакового 16-битного числа | Numeric conversion завершается ошибкой вместо тихого переполнения. |
| В запечённом спрайте нет кадров или направлений | Клиентская загрузка не проходит validation. |
| Запечённый кадр является sprite reference | Копируются спрайт и offset кадра по ссылке. |
Сумма цикла walk/run T равна нулю |
Выбирается кадр 0, root-motion offset движения остаётся нулевым. |
| Нет активного движения | Root motion walk/run не перемещает логический или отображаемый криттер через эту ветку. |
| Анимация не является walk/run | Offsets визуально накапливаются до выбранного кадра. |
| Криттер использует 3D-модель | Ветка root motion 2D-спрайта пропускается. |
| Целевой гекс при остановке находится вне карты или заблокирован | Нормализация отклоняется; текущий логический гекс и offset сохраняются. |
Матрица визуальной приёмки
| Маршрут | Собираемое evidence | Признак ошибки |
|---|---|---|
| Прямая ходьба и бег | Стабильный cadence на нескольких полных циклах каждой поддерживаемой скорости | Foot sliding, обратный порядок poses или периодический скачок |
| Все созданные направления | Правильный вектор displacement и сопоставимая длина цикла | Отражённая ось, диагональный drift или различающийся темп |
| Поворот во время движения | Phase продолжается при замене sheet | Походка перезапускается или спрайт скачет на каждом повороте |
| Быстрый stop/start | Логический гекс догоняет позицию без отображаемого скачка | Растущий offset, отделившийся idle sprite или цикл blocked-path resync |
| Граница гекса и reconciliation | Мировая позиция спрайта непрерывна при re-partition гекс/offset | Залипание на краю клетки или скачок на один гекс |
| Extraction, merge или substitution | Выбранный sheet сохраняет ожидаемые cumulative offsets по обеим осям | Первый добавленный кадр скачет или одна ось переносится неверно |
| Fallback с нулевой суммой | Стабильный кадр 0 без root-motion offset |
Деление на ноль, нестабильный выбор кадра или drift |
| Поддерживаемые zoom/renderers | Foot placement остаётся приемлемым в сцене проекта | Зависящий от zoom jitter или неприемлемый cadence |
Автоматические тесты baker и geometry подтверждают данные и нормализацию. Они не заменяют видимую клиентскую приёмку качества походки.
Практики создания контента
- Считайте
NextX/NextYпокадровым визуальным displacement, а не абсолютными координатами кадра. - Создавайте и проверяйте каждое направление. Поддерживайте согласованные по направлению cycle vectors, чтобы повороты сохраняли убедительную фазу.
- Сохраняйте сумму
Tненулевой для locomotion sheet, который должен пространственно управлять походкой. - Согласуйте порядок offsets с порядком poses. Большая коррекция в одном кадре создаёт видимый скачок даже при правильной сумме цикла.
- Проверяйте mirroring, extraction, merging и substitutions первого/последнего кадра: эти операции могут изменить накопленные offsets.
- Не настраивайте скорость пути пикселями root motion. Настройте авторитетное движение правилами проекта, затем согласуйте с ним визуальный цикл.
- Проверяйте быстрые смены направления и stop/start. Эти маршруты проверяют передачу anchor и нормализацию offset, а не только равномерное прямое движение.
- Проверяйте поддерживаемые проектом zoom, geometry и renderer configurations. Алгоритм переиспользуем, но приемлемый cadence и foot sliding являются решениями контента.
Project evidence и правила извлечения
Закреплённый snapshot Last Frontier направляет работу с 2D locomotion через Docs/ContentWorkflow.md и требует согласовывать изменения Engine по Docs/DocumentationMaintenance.md. Текущий character pipeline в основном использует .fo3d; Docs/CharacterGenerator.md пропускает импортированную translation из 3D-клипов *_RM, потому что мировым перемещением владеет Engine. Это полезная граница владения 3D, но не evidence конкретных значений 2D NextX / NextY.
В проверенных resource trees Last Frontier и FOnline TLA нет текущих созданных .fofrm или legacy FRM-family sprite assets, по которым можно подтвердить ненулевой production-цикл. Docs/Animation.md из TLA сохраняет полезное историческое объяснение projection цикла ходьбы, но имена raw_ptr<const SpriteSheet> и DefaultSpriteFactory::LoadAnimation устарели. Текущий Engine использует nptr<const SpriteSheet> и DefaultSpriteFactory::LoadSprite.
Поэтому в переиспользуемый контракт не переносятся внешние значения offsets, утверждения о качестве походки или проектные скорости. Извлекайте только межпроектные концепции, записывайте точные snapshot paths и commits в evidence model, заново выводите результат из текущих исходников Engine и оставляйте визуальную приёмку в проекте-владельце.
Граница проекта
Engine владеет импортом исходных форматов, переносом запечённых offsets, sheet по направлениям, авторитетной интерполяцией движения, anchor/phase math, выбором кадра, финальным клиентским выравниванием и stop-time нормализацией гекса/offset.
Встраиваемый проект владеет sprite assets, композицией FOFRM, animation substitutions, state/action mapping, скоростями движения, выбором геометрии, визуальными критериями качества и регрессиями сцен. Руководство проекта может описывать конкретный art pipeline и tuning values, но должно ссылаться сюда, а не повторять runtime-алгоритм.
Триггеры сопровождения
Повторно проверяйте эту страницу и focused tests, когда изменение затрагивает любую из следующих поверхностей:
FrameShot::NextX/NextY, версию SpriteResource,NextOffset, shared-frame behavior илиSpriteSheet::_sprOffset;- импорт FOFRM/legacy images, mirroring, cropping, frame extraction, animation merge, cloning или substitution;
MovingContext::BuildProgress, интерполяцию гекс/offset, prediction reconciliation илиGeometryHelper::NormalizeHexOffset;- lifecycle движения
CritterHexView, активацию walk/run, передачу anchor, выбор кадра,_offsAnim, проходимость карты или ограничение light offset; - обновление закреплённого Last Frontier или TLA, меняющее соответствующие assets, project policy или historical evidence.
При обновлении внешнего проекта или Engine сначала выполните fetch, запишите старый/новый диапазон commits, проверьте весь входящий диапазон, обновите BuildTools/ExternalProjectEvidence.json, перегенерируйте site/AI/localization artifacts в порядке зависимостей и сохраняйте независимость страницы от project-only helpers.
Маршруты проверки
Для изменений документации:
python BuildTools/tests/test_docs_sprite_root_motion.py
python BuildTools/docs_external_evidence.py --check
python BuildTools/docs_localization.py --check
python BuildTools/docs_site.py --check
python BuildTools/docs_ai_delivery.py --check
python BuildTools/docs_validate.py
Для импорта offsets и переноса запечённых данных запустите тесты ImageBaker в Engine test binary встраиваемого проекта. Test_ImageBaker.cpp покрывает знаковые FRM offsets, lower/camel-case keys FOFRM, вложенную композицию, mirroring, выбор кадров и baked round trips. Test_Geometry.cpp покрывает успешный re-split, отказ для заблокированной цели, поведение внутри текущего гекса, unrestricted normalization и отказ за пределами карты.
В текущем native suite нет focused fixture для root motion CritterHexView. После изменения MovingContext, CritterHexView, SpriteSheet, выбора анимации ResourceManager или созданных locomotion offsets выполните матрицу визуальной приёмки в видимой клиентской сцене. Baking подтверждает data path, geometry tests - re-split, а видимая сцена - согласование походки.