Решили: направление удара для формы замеряется по клипу один раз при инициализации вида (два сэмпла вокруг кадра контакта, разница положений кончика оружия), а не считается из позы в момент события урона.
Почему: позы в момент события ещё нет. Событие приходит внутри Update презентера, у которого
[DefaultExecutionOrder(-100)], — то есть до Update самих видов и заведомо до того, как Animator
применит позу, потому что он делает это между Update и LateUpdate. Читаешь кости — получаешь
прошлый кадр. Отвергнуты: чтение позы в момент события (то, что стояло полдня, ошибка 15–100°
в зависимости от скорости атаки) и снимок в LateUpdate по прохождении маркера (дешевле, но
заводит гонку: событие урона может прийти раньше, чем вид перешагнёт маркер, а фолбэк запрещён —
знак удара тогда просто пропадал бы).
Сказано: «Ты уверен, что все ок? Удар сверху вниз, а этот эффект идет под очень странным углом. Ты как определяешь точку начала теперь?» (06.08.2026, сессия a4e2eebd).
Грабли:
- Прецедент лежал в соседнем файле.
SwingArcVfxчитает геометрию вLateUpdateи объясняет это прямым комментарием: «плечо двигает Animator, и в Update дуга отставала бы от руки на кадр». Тот же порядок кадра ударил по форме, и его стоило проверить сразу, а не после второго скриншота. - Ошибка масштабируется скоростью атаки. Взмах — 200° за время замаха; у быстрого кита замах укладывается в два-три кадра показа, и «отставание на кадр» превращается в 60–100°. На медленных китах то же самое выглядит как небольшая неточность, поэтому по одному бою диагноз не ставится.
SampleAnimationработает ЧЕРЕЗ Animator, и на выключенном не делает ничего. Первая версия замера выключала его «чтобы не переписал позу между выборками» — и получала нулевое движение. Переписывать ему нечего: он ставит позу в своей фазе, междуUpdateиLateUpdate, а обе выборки идут подряд.- Сэмплировать надо КОРЕНЬ ТЕЛА, а не объект с Animator. На префабе вида Animator сидит в соседней
ветке (
Visual Sprites/Body), кости живут подVisual Sprites/BoneVisual, а пути клипа начинаются сHips/…— они разрешаются только от второго. От первого клип применяется в пустоту, и замер честно рапортует «кончик стоит на месте». Обе ошибки выглядят одинаково, поэтому диагноз по сообщению не ставится — нужен путь иерархии, и он теперь печатается в падении теста. - Инвариант закрыт тестом (
StrikeDirectionTests): он грузит живой префаб и живой клип, гоняет тот же замер и требует, чтобы направление шло ВНИЗ. Обе тихие поломки он ловит с первого прогона. - Шаг замера имеет коридор. Слишком мелкий ловит шум кривой, слишком крупный усредняет дугу и врёт в ту же сторону, что и отменённая хорда. Взято два кадра показа с зажимом в [1/120, 1/30].
- Замер живёт в координатах КОРНЯ ТЕЛА, а не вида. Разворот юнита сделан знаком масштаба на этом
же узле, поэтому
TransformDirectionвозвращает уже зеркальный вектор — отдельный флаг «смотрит влево» не нужен. Побочно: сплющивание тела слегка искажает угол, но живёт доли секунды. - Рука не жёсткий рычаг. Промежуточный вариант «перпендикуляр к вектору плечо → кончик» опирался на это допущение; замер живого клипа даёт разброс длины рычага в 8% между началом взмаха и контактом. Само по себе терпимо, но вместе с отставанием кадра дало наблюдаемый «странный угол».
Владелец правды: UnitView.MeasureStrikeDirection (замер) и UnitView.TryGetStrikeDirection
(выдача). Дизайн-сторона — канон gdd/70-gamefeel/vfx-language §«Серп лежит вдоль направления
удара» и запись журнала ГД 2026-08-06/3.