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

Работа с анимациями — ключевая часть геймдева. Phaser 3 предлагает мощный API для создания последовательностей твинов через `tweens.chain()`. Однако в его использовании есть нюанс, который может сбить с толку: почему событие `onStart` в конфигурации цепочки и отдельный слушатель `TWEEN_START` ведут себя по-разному? Эта статья разберет пример кода, демонстрирующий эту особенность, и покажет, как правильно управлять событиями в цепочках анимаций.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    create() {
        this.objToTween = this.add.circle(this.scale.width / 2,this.scale.height / 2,50,0xff0000);
    
        const chain = this.tweens.chain({
          tweens: [
            { targets: this.objToTween, duration: 500, props: { alpha: 0 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 1 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 0 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 1 } },
          ],
          onStart: () => console.log('chain start!')
        });
    
        chain.on(Phaser.Tweens.Events.TWEEN_START, () => console.log('added on start listener')); // I assume this should work
      }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d88',
    scene: Example
};

const game = new Phaser.Game(config);

Что делает пример?

В данном примере создается простая сцена, в которой красный круг поочередно исчезает и появляется четыре раза. Эта анимация реализована не отдельными твинами, а единой цепочкой (chain).

class Example extends Phaser.Scene
{
    create() {
        this.objToTween = this.add.circle(this.scale.width / 2,this.scale.height / 2,50,0xff0000);

Сначала в центре экрана создается графический объект — красный круг. Его позиция вычисляется динамически, относительно размеров игрового окна (this.scale.width / 2).

Создание цепочки твинов

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

const chain = this.tweens.chain({
          tweens: [
            { targets: this.objToTween, duration: 500, props: { alpha: 0 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 1 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 0 } },
            { targets: this.objToTween, duration: 500, props: { alpha: 1 } },
          ],
          onStart: () => console.log('chain start!')
        });

Ключевые параметры: * tweens: Массив объектов-конфигураций для каждого твина в цепочке. Здесь четыре одинаковых по длительности (500 мс) твина, которые меняют свойство alpha (прозрачность) цели (targets) — нашего красного круга — между 0 (невидимый) и 1 (полностью видимый). * onStart: **Callback-функция**, которая вызывается один раз — в момент запуска всей цепочки. В консоль выведется сообщение 'chain start!'.

Нюанс: Событие TWEEN_START vs onStart

После создания цепочки к ее объекту добавляется еще один слушатель события. И здесь проявляется важное различие в API Phaser.

chain.on(Phaser.Tweens.Events.TWEEN_START, () => console.log('added on start listener'));

* chain.on(...): Этот метод добавляет слушатель на событие Phaser.Tweens.Events.TWEEN_START. **Важно:** Это событие генерируется не для всей цепочки, а для КАЖДОГО отдельного твина внутри нее в момент его старта. В нашем примере сообщение 'added on start listener' появится в консоли четыре раза — по разу для каждого из четырех твинов. * onStart в конфигурации: Это callback, который срабатывает строго один раз — при запуске первого твина в цепочке (т.е. при старте всей последовательности).

Таким образом, onStart — это событие старта цепочки, а TWEEN_START — событие старта каждого ее звена. Их нельзя использовать как взаимозаменяемые.

Конфигурация игры и запуск

Код завершается стандартной для Phaser 3 конфигурацией игрового экземпляра.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#2d2d88',
    scene: Example
};

const game = new Phaser.Game(config);

Здесь создается игра с размером холста 800x600 пикселей, темно-синим фоном (#2d2d88) и единственной сценой — классом Example, логика которого была описана выше.

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

Использование tweens.chain() — отличный способ создавать сложные последовательные анимации без ручного управления временными интервалами. Главный вывод: четко различайте события для всей цепочки (onStart в конфиге) и события для отдельных ее элементов (TWEEN_START). Для экспериментов попробуйте: добавить слушатель TWEEN_COMPLETE для отслеживания завершения каждого твина; изменить цепочку, чтобы твины двигали круг по экрану (`x,y); или создать вложенную цепочку, запускаемую из callback'аonComplete` другой.