Смещения кадров спрайтов и циклы ходьбы
FOnline переносит запечённые 2D-дельты кадров и согласует позы walk/run с авторитетным движением по гексам. Проект владеет art, animation mapping, настройкой движения и визуальной приёмкой.
Статус контракта
Исходники/тесты Engine определяют независимый 2D-контракт переноса offsets,
выбора кадров по движению, передачи фазы и нормализации при остановке.
Last Frontier/TLA — discovery и compatibility evidence, закреплённые в
BuildTools/ExternalProjectEvidence.json; их art, policy и исторические имена
не расширяют контракт. Выводите переиспользуемые утверждения из Engine.
Форматы изображений и спрайтов описывают импорт, FOFRM,
контейнеры, загрузку, atlases и caches. Анимация/длительности .fo3d принадлежат
анимации моделей.
Область и источники истины
Контрактом владеют следующие исходники:
Source/Tools/ImageImport.*-FrameShot::NextX/NextYи общий контекст загрузчика;Source/Tools/ImageBaker.*- координация, преобразования и сериализация запечённых спрайтов; соседние парыImageBaker-*.h/.cppвладеют импортом исходных форматов;Source/Common/SpriteResource.*- общий декодер кадров SpriteResource v2 и переносNextOffset;Source/Client/DefaultSprites.*— принадлежащие листу motion, загрузка запечённых данных, общие кадры и листы по направлениям;Source/Common/Movement.*- авторитетный прогресс времени/пути и построение целочисленного пиксельногоHexOffset;Source/Common/Geometry.*- re-split без потери позиции между гексом и offset с учётом проходимости;Source/Client/AnimationMovement.*— чистая математика накопления дельт, проекции цикла, переноса фазы, выбора кадра и визуальной компенсации;Source/Client/CritterHexView.*— anchor цикла, displacement движения, применение animation offset и нормализация при остановке;Source/Client/CritterFramesCache.cpp- кеш листов по направлениям и тегов анимации, разрешённых проектом; проект выбирает, извлекает и склеивает кадры через общий API спрайтов;Source/Tests/Test_Mapper.cpp,Source/Tests/Test_ImageBaker.cppиSource/Tests/Test_Geometry.cpp— математика цикла, реальный потребитель CritterHexView, перенос формата и нормализация геометрии.
Документация и контент проектов полезны как integration evidence, но не являются источником истины для переиспользуемого поведения Engine.
Три независимые позиции
Для движущегося 2D-криттера участвуют три связанные величины:
- Логическое движение:
MovingContextвладеет прогрессом пути, текущим гексом, направлением, завершением и скоростью движения. - Интерполяция внутри гекса:
MovingContext::BuildProgressпреобразует плавную позицию сегмента в целочисленные пиксельные значенияHexOffset.CritterHexView::ProcessMovingприменяет логический гекс и offset. - Авторское смещение спрайта: каждый кадр анимации содержит целочисленную delta
(NextX, NextY).CritterHexViewвыбирает кадр и вычисляет_offsAnim, чтобы отображаемый спрайт следовал созданному циклу ходьбы, пока логическое движение остаётся авторитетным.
Смещения кадров — данные представления. Они не перемещают сущность, не выбирают путь, не меняют скорость, не разрешают переход между гексами и не реплицируют движение по сети.
Создание покадровых offsets
ImageImport::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. Для frame-offset 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, а не предполагайте, что каждый формат предоставляет одинаковые авторские поля.
Опциональный импортер Arcanum ART сохраняет DeltaX/DeltaY как NextX/NextY,
включая соответствующие знаки после отражения. Точки привязки кадров вместо
этого совмещаются на общем холсте и определяют размещение последовательности;
они не задают смещение между кадрами. FOFRM Alias сохраняет оба вида смещений,
а обычная композиция копирует только дочерние кадры и добавляет авторские
межкадровые смещения. Ротации и размещение описывает
контракт импорта ART.
Смещения зависят от направления. Выбор кадров, опорных смещений и склейка принадлежат игровым скриптам; движок хранит переданные значения через общий API. См. скриптовые анимации.
Запечённое и runtime-представление
ImageBaker::BakeCollection записывает NextX и NextY каждого конкретного кадра после преобразований как знаковые 16-битные значения в запись кадра SpriteResource v2. Запись deduplicated shared frame ссылается на более ранний кадр. ReadSpriteResource декодирует пару в SpriteResourceFrameData::NextOffset вместе с draw offset, размерами, пикселями и опциональной mesh кадра.
DefaultSpriteFactory::LoadSprite передаёт декодированные дельты NextOffset в
SpriteSheet::SetFrame(index, sprite, offset, motion). Shared frame сохраняет исходный
спрайт атласа и независимо копирует offset и motion. Бинарный формат
и авторские ключи NextX/NextY не меняются.
SpriteSheet::GetSprOffset(frame) читает обычное смещение спрайта до рута из
_sprOffset. GetSprMotion()[frame] читает покадровую дельту визуального переноса
из _sprMotion. SetFrame(index, sprite, offset, motion) записывает каждый вход
в соответствующее поле. Копии листов содержат независимые значения.
CritterHexView::RefreshAnimOffset передаёт накопление дельт motion и согласование позы
в AnimationMovement, сохраняя выбранный лист и anchor относительно пути. Отдельного объекта motion и системы перемещения
персонажа по анимации нет.
Game.GetSpriteFrameMotion читает дельту кадра; Game.CreateSpriteSequence хранит
переданные motions отдельно от абсолютных draw offsets. Выбор кадров, накопление
для последней позы и склейка последовательностей принадлежат проекту.
Созданные листы разделяют исходные кадры AtlasSprite. Независимые абсолютные опоры
отрисовки хранятся в SpriteSheet::_sprOffset. SetAnimSpr выбирает только
изображение и draw anchor. RefreshAnimOffset вместе выбирает позу по движению и её
визуальную компенсацию до применения изображения; frozen/timed кадры задаёт playback.
RefreshOffs однократно складывает
HexOffset + extra displacement + _offsAnim. MapSprite применяет draw anchor
только к размещению изображения, не добавляя его в motion или логический мировой
корень. Ни одно из этих значений не меняет прохождение пути.
Когда движение выбирает кадр
Ветка движения в CritterHexView::Process активна, только когда выполняются все условия:
- криттер использует 2D-спрайт, а не загруженную 3D-модель;
- у криттера есть активный
MovingContext; - проект вернул
CritterAnimationFlags::MovementбезFreezeизOnCritterAnimationInit.
В этой ветке кадр выбирает позиция движения. Прошедшее время анимации не продвигает цикл ходьбы/бега независимо.
Без флага Movement RefreshAnimOffset накапливает offsets до выбранного кадра включительно в _offsAnim. Это позволяет авторскому action визуально смещаться, пока логический криттер остаётся привязанным к месту. Sheet walk/run без активного движения не считается самостоятельной командой движения сущности.
Непрерывный 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.
Остановка и нормализация 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.
При смене листа RefreshAnimOffset устанавливает привязку до выбора кадра и расчёта компенсации, включая RefreshView перед следующим Process. Чистая математика находится в AnimationMovement, состояние привязки — в CritterHexView; изображение и его draw anchor в расчёты не входят.
Выбор кадра
AnimationMovement::EvaluateWalkFrame проецирует каждое накопленное авторское смещение на 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 функция AnimationMovement::EvaluateWalkFrame вычисляет целый номер цикла 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]
Размещение map sprite включает логический гекс и HexOffset. offs_anim
компенсирует линейное движение внутри кадра и ставит спрайт в накопленный
авторский offset. Смена кадра продвигает визуал на следующую delta, пока
логическое движение продолжается.
Это выравнивание, а не locomotion, управляемая анимацией. Изменение NextX / NextY меняет визуальный шаг и выбор фазы, но не заставляет server-side entity двигаться дальше или быстрее.
Смена направления и sheet
Поворот внутри одного MovingContext обычно выбирает другой direction-specific SpriteSheet с иным T и, возможно, числом кадров. Перезапуск с нулевой фазы визуально сбрасывал бы походку на каждом повороте.
При смене выбранного листа RefreshAnimOffset вызывает AnimationMovement::TransferWalkPhase до выбора кадра и расчёта смещения, включая RefreshView до следующего Process. Владельцем нового anchor остаётся CritterHexView: он сдвигает привязку так, чтобы projection на новый cycle vector начиналась с той же доли:
old_phase = old_cycle_proj / dot(T_old, T_old)
new_anchor = pos - round(old_phase * T_new)
Локальная функция модуля ScaleWalkPhase точно масштабирует компоненты с округлением к ближайшему целому со знаком (половины от нуля), без переполнения промежуточного числителя. Если прежний 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, frame-offset offset движения остаётся нулевым. |
| Нет активного движения при Movement без Freeze | Ветка согласования не применяет поправку анимации. |
| Нет флага Movement | Offsets визуально накапливаются до выбранного кадра. |
| Криттер использует 3D-модель | Ветка смещений кадров 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 без frame-offset offset |
Деление на ноль, нестабильный выбор кадра или drift |
| Поддерживаемые zoom/renderers | Foot placement остаётся приемлемым в сцене проекта | Зависящий от zoom jitter или неприемлемый cadence |
Автоматические тесты baker и geometry подтверждают данные и нормализацию. Они не заменяют видимую клиентскую приёмку качества походки.
Практики создания контента
NextX/NextY— покадровые дельты в пикселях, а не абсолютные координаты.- Проверяйте каждое направление и согласованность cycle vectors при поворотах.
- Сохраняйте
Tненулевым для locomotion sheets, пространственно управляющих походкой. - Согласуйте deltas с poses: большая коррекция одного кадра скачет даже при правильной сумме.
- Проверяйте изменения offsets при mirroring, extraction, merges и first/last substitutions.
- Настройте скорость авторитетным движением проекта, затем согласуйте визуальный цикл.
- Проверяйте anchor transfer и normalization при быстрых поворотах и stop/start.
- Проверяйте поддерживаемые zoom, geometry и renderers; приёмка cadence/foot sliding принадлежит проекту.
Project evidence и правила извлечения
Закреплённый Last Frontier направляет 2D-работу через Docs/ContentWorkflow.md,
согласование Engine — через Docs/DocumentationMaintenance.md. В основном
.fo3d pipeline пропускает translation *_RM в Docs/CharacterGenerator.md:
Engine владеет мировым движением. Это не подтверждает значения 2D NextX/NextY.
Проверенные LF/TLA trees не содержат авторского FOFRM/FRM production cycle с
подтверждённым ненулевым displacement. TLA Docs/Animation.md исторически
объясняет projection; его raw_ptr<const SpriteSheet>/
DefaultSpriteFactory::LoadAnimation устарели. Текущие имена —
nptr<const SpriteSheet>/DefaultSpriteFactory::LoadSprite.
Извлекайте только концепции, записывайте точные paths/commits в evidence model, выводите результат из Engine. Скорости, offsets и визуальная приёмка принадлежат проекту.
Граница проекта
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::_sprMotion;- импорт FOFRM/legacy images, mirroring, cropping, frame extraction, animation merge, cloning или substitution;
MovingContext::BuildProgress, интерполяцию гекс/offset, prediction reconciliation илиGeometryHelper::NormalizeHexOffset;- накопление дельт, проекцию цикла, перенос фазы, выбор кадра и визуальную компенсацию в
AnimationMovement; - lifecycle движения
CritterHexView, активацию правилами проекта, владение anchor,_offsAnim, проходимость карты; - обновление закреплённого 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 и отказ за пределами карты.
CritterAnimationPreservesProjectSelectionWhileMoving проверяет реальный потребитель криттера с произвольными идентификаторами, явным флагом движения и замороженными кадрами. После изменения MovingContext, CritterHexView, SpriteSheet, CritterFramesCache или выбора анимации проекта или созданных locomotion offsets выполните матрицу визуальной приёмки в видимой клиентской сцене. Baking подтверждает data path, geometry tests - re-split, а видимая сцена - согласование походки.