Newer
Older
rpg / docs / engine / anim.md

Анимация: клипы, аниматоры, оживители, реестр тика

Все анимации движка тикаются от фиксированного шага (60 Гц) — Pixi-тикер не используется (sharedTicker: false), Pixi AnimatedSprite не применяется. Чистая математика (клипы, оживители, симуляция частиц) живёт отдельно от тонких Pixi-адаптеров и тестируется в Vitest без браузера.

Модули:

anim/clip.ts           чистый степпер клипов (loop/once/pingpong)
anim/SpriteAnimator.ts кадры → Sprite, именованные клипы, события
anim/motion.ts         чистые «оживители» статики (bob/sway/pulse/blink)
anim/SpriteMotion.ts   Pixi-адаптер оживителей (view → колебания)

Реестр тика: engine.fx

Эффекты, аниматоры и вспышки не требуют ручного update(dt) из сцены — добавьте объект в engine.fx (core/Updatables), движок тикает его сам между твин-ами и логикой сцены:

engine.fx.add(emitter);          // возвращает тот же объект
engine.fx.add(new SpriteMotion(view, { bob: {...} }, { seed: 7 }));
engine.fx.remove(fx);            // редко: обычно достаточно destroyed
engine.fx.clear();               // при выходе из сцены, если она не разрушает дерево

Самозачистка: объект с destroyed === true не тикается и вычищается из реестра после очередного прохода (Pixi destroy() выставляет флаг сам — эффекты, живущие в разрушаемом дереве мира, отпадают без кода; SpriteAnimator и SpriteMotion проксируют destroyed своего вью). Допускается и локальный инстанс Updatables внутри сцены (тот же класс, свой тик).

Клипы и SpriteAnimator

import { SpriteAnimator } from '@rpg/engine';

const animator = new SpriteAnimator(sprite, {
    walk_down: { frames: assets.frames('chars/hero_sheet.json', 'hero_down'), fps: 6 },
    idle:      { frames: [assets.frames('chars/hero_sheet.json', 'hero_down')[0]], loop: 'once' }
}, 'walk_down', { randomPhase: seed });   // seed — рассинхрон толпы

animator.play('walk_down', { restart: true }); // тот же клип без restart — продолжение
animator.pause();          // ... resume(), showStep(0) — заморозить на шаге
animator.onFrame = (step) => ...;    // смена шага
animator.onFinish = (clip) => ...;   // конец once/pingpong-прохода
animator.setClipFrames('walk_down', otherFrames); // кадры на лету (вариации окраса)

Режимы клипа (ClipLoop): 'loop' — цикл; 'once' — один раз и стоп на последнем кадре (после конца аниматор сам паузится); 'pingpong' — туда-обратно без дубля крайних кадров. Математика — чистая stepClip в anim/clip.ts: время накапливается в «шагах», dt больше периода проматывает промежуточные шаги (сообщается только финальный) — по аналогии с пересечением delay в Tween.

sprite.texture пишется только при смене шага. Флип и anchor — зона владельца спрайта (см. рецепт ходьбы в recipes.md).

Оживители статики: SpriteMotion

Процедурное движение без кадров — для всего, что не умеет ходить: парящие сгустки, качающиеся колокольчики, пульсирующие искры, мигающие огни. Математика — чистая sampleMotion(params, t) (тестируется без Pixi), Pixi-адаптер — SpriteMotion:

import { SpriteMotion } from '@rpg/engine';

const motion = new SpriteMotion(bellView, {
    sway: { amplitude: 0.06, period: 2.2 }   // рад вокруг основания
}, { seed: tileY * 64 + tileX });            // рассинхрон кустов — детерминирован
engine.fx.add(motion);

Параметры (комбинируются, эффекты перемножаются):

Параметр Движение Для чего
bob { amplitude, period } парение по Y (px, округлено до целого) сгустки, духи, всё «невесомое»
sway { amplitude, period } покачивание рад вокруг основания колокольчики, цветы, трава — укоренённое
pulse { min, max, period } пульс alpha и scale треугольной волной свечение искр, магические предметы
blink { period, duty? } строб 0/1 (duty — доля «включённого») огни, маяки, акценты

Правила выбора: bob — только для объектов, не имеющих контакта с землёй (парящие), иначе рвётся «тень-контакт»; sway — для укоренённых (поворот вокруг основания сохраняет контакт); pulse/blink — для свечения и акцентов.

SpriteMotion снимает базовые position/rotation/alpha/scale в конструкторе — позиционируйте view до его создания. Важно: позиция/поворот перезаписываются каждый тик только когда есть bob/sway — view с чистым pulse/blink (светлячок, мигающий герой) можно двигать извне, оживитель позицию не тронет. params живые: меняйте на лету (колокол качается только пока звонят). detach() возвращает исходные значения (только тех полей, которые трогал). Фаза из seed — детерминированная (никакого Math.random).

Анимированные тайлы: setTileAnimation

Вода и прочие «живые» ground-тайлы — не спрайтовые аниматоры, а один общий таймлайн на все клетки с данным id (дёшево при сотнях клеток воды):

map.setTileAnimation(TILES.WATER, [assets.texture('tiles/water_1'),
                                   assets.texture('tiles/water_2')], 2); // fps
// в update сцены (no-op, если анимаций нет):
map.update(dt);

Кадры — обычные текстуры (например, из tiles/water_1.png/water_2.png); клетки, нарисованные до вызова, регистрируются автоматически, setTile перерегистрирует при перерисовке.