О чем этот пример

Работа с анимациями — неотъемлемая часть разработки игр. Часто разработчики сталкиваются с ситуацией, когда твин внутри таймлайна ведёт себя не так, как ожидалось, особенно при использовании колбэка `onUpdate`. В этой статье мы разберём пример, где `onUpdate` не вызывается, и покажем, как исправить эту проблему. Понимание этих нюансов сэкономит вам часы отладки и позволит создавать более сложные и контролируемые последовательности анимаций.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('ship', 'assets/tweens/spacetank.png');
    }

    create ()
    {
        const ship1 = this.add.image(200, 300, 'ship');
        const ship2 = this.add.image(600, 300, 'ship');

        // this.tweens.add({
        //     targets: [ ship1, ship2 ],
        //     alpha: {
        //         from: 1,
        //         to: 0,
        //         yoyo: true,
        //         duration: 500,
        //         ease: Phaser.Math.Easing.Sine.InOut,
        //     },
        //     onUpdate: (t) => {
        //         console.log(t.progress);
        //     }
        // });

        this.cameras.main.setBackgroundColor('rgb(87,255,0)');

        const timeline = this.add.timeline([
            {
                at: 1000,
                tween: {
                    targets: [ ship1, ship2 ],
                    alpha: {
                        from: 1,
                        to: 0,
                        yoyo: true,
                        duration: 500,
                        ease: Phaser.Math.Easing.Sine.InOut,
                    },
                    onUpdate: (t) => {
                        console.log(t.progress);
                    }
                }
            }
        ]);

        timeline.play();



    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: 'rgb(87,255,0)',
    clearBeforeRender: false,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Проблема: onUpdate не срабатывает в таймлайне

В исходном коде представлен класс сцены, который создаёт два спрайта корабля и пытается анимировать их прозрачность с помощью Phaser.Tweens.Timeline. Ключевая проблема — колбэк onUpdate, переданный в конфигурацию твина внутри таймлайна, никогда не вызывается.

Это происходит потому, что структура конфигурации твина, когда он объявляется внутри события таймлайна, отличается от структуры при прямом вызове this.tweens.add(). В исходном примере onUpdate помещён на один уровень с параметрами анимации, что некорректно для контекста таймлайна.

Анатомия таймлайна в Phaser

Таймлайн (Phaser.Tweens.Timeline) — это мощный инструмент для упорядочивания твинов во времени. Он создаётся методом this.add.timeline(), который принимает массив событий. Каждое событие описывается объектом с двумя основными полями:

* at: Время в миллисекундах от начала воспроизведения таймлайна, когда должен стартовать твин. * tween: Объект с конфигурацией для создания твина. Именно его структура часто вызывает путаницу.

Вот как выглядит создание таймлайна в коде:

const timeline = this.add.timeline([
    {
        at: 1000,
        tween: {
            targets: [ ship1, ship2 ],
            alpha: { from: 1, to: 0, yoyo: true, duration: 500 }
        }
    }
]);
timeline.play();

Исправление: правильное размещение onUpdate

Чтобы колбэк onUpdate начал работать, его необходимо поместить не на верхний уровень конфигурации tween, а внутрь объекта, описывающего конкретное анимируемое свойство (в нашем случае — alpha). Это ключевое отличие от стандартного создания твина.

Вот исправленная версия конфигурации для события в таймлайне. Обратите внимание, что onUpdate теперь находится внутри alpha:

const timeline = this.add.timeline([
    {
        at: 1000,
        tween: {
            targets: [ ship1, ship2 ],
            alpha: {
                from: 1,
                to: 0,
                yoyo: true,
                duration: 500,
                ease: Phaser.Math.Easing.Sine.InOut,
                onUpdate: (tween) => {
                    console.log(tween.progress);
                }
            }
        }
    }
]);

Колбэк onUpdate теперь будет вызываться на каждом кадре анимации свойства alpha, и в консоль будет выводиться текущий прогресс твина (значение от 0 до 1).

Сравнение с обычным твином

Давайте проясним разницу. При создании твина напрямую через this.tweens.add(), колбэки размещаются на том же уровне, что и targets. Это работает корректно.

Пример прямого создания твина (раскомментируйте в исходном коде):

this.tweens.add({
    targets: [ ship1, ship2 ],
    alpha: { from: 1, to: 0, yoyo: true, duration: 500 },
    onUpdate: (t) => { console.log(t.progress); } // onUpdate здесь
});

Однако внутри таймлайна структура вложенная. Объект tween внутри события — это почти полный аналог конфига для this.tweens.add(), но с важным исключением: глобальные колбэки (как onUpdate) для всего твина в этом контексте не поддерживаются. Вместо этого они должны быть привязаны к конкретному свойству.

Что попробовать дальше

Главный вывод: при работе с Phaser.Tweens.Timeline размещайте колбэк onUpdate внутри конфигурации анимируемого свойства (например, alpha, `x,scale), а не на корневом уровне объектаtween`. Это обеспечит его корректный вызов. Для экспериментов попробуйте: 1. Добавить в таймлайн несколько событий с разными onUpdate, чтобы отслеживать анимацию нескольких свойств независимо. 2. Использовать onUpdate для привязки логики игры к прогрессу анимации (например, менять цвет объекта в зависимости от tween.progress). 3. Поэкспериментировать с другими колбэками, такими как onComplete, внутри свойства, чтобы понять общий принцип.