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

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

Версия 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.atlas('assets', 'assets/atlas/tweenparts.png', 'assets/atlas/tweenparts.json');
	}

	create ()
	{
		const onActive = function (tween, targets)
		{
			console.count(`onActive ${targets[ 0 ].name}`);
		};

		this.tweens.chain({
			loop: 1,
			onActive: function () { console.log('🔗 onActive Chain 🔗') },
			onLoop: function () { console.log('🔗 onLoop Chain 🔗') },
			tweens: [
				{ targets: { name: 'A', value: 0 }, value: 1, onActive },
				{ targets: { name: 'B', value: 0 }, value: 1, onActive },
				{ targets: { name: 'C', value: 0 }, value: 1, onActive },
			]
		});
	}
}

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

const game = new Phaser.Game(config);

Разбираем пример: цепочка из трех твинов

В данном примере создается сцена, которая загружает атлас и в методе create() сразу запускает цепочку твинов. Цель кода — отследить порядок и количество вызовов коллбэков, особенно onActive.

const onActive = function (tween, targets) {
    console.count(`onActive ${targets[0].name}`);
};

Здесь объявляется общая функция onActive. Она принимает объект твина и массив целей анимации. console.count выводит в консоль имя первой цели (targets[0].name) и подсчитывает, сколько раз была вызвана эта конкретная строка. Это ключевой инструмент для обнаружения проблемы.

Создание цепочки: структура и коллбэки

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

this.tweens.chain({
    loop: 1,
    onActive: function () { console.log('🔗 onActive Chain 🔗') },
    onLoop: function () { console.log('🔗 onLoop Chain 🔗') },
    tweens: [
        { targets: { name: 'A', value: 0 }, value: 1, onActive },
        { targets: { name: 'B', value: 0 }, value: 1, onActive },
        { targets: { name: 'C', value: 0 }, value: 1, onActive },
    ]
});

* loop: 1: Цепочка выполнится один раз, а затем повторит всю последовательность еще один раз (итого — два прохода). * onActive и onLoop на уровне цепочки: Эти коллбэки срабатывают для всего объекта цепочки, а не для отдельных твинов. * Массив tweens: Каждый элемент — конфиг для отдельного твина. Все они анимируют свойство value от 0 до 1 у простого объекта с полем name. Важно, что каждый твин передает ссылку на одну и ту же внешнюю функцию onActive.

Суть ошибки: почему `onActive` вызывается дважды?

В версиях Phaser 3, где существовал этот баг (например, в примере под номером 6773), наблюдалось следующее поведение:

1. При запуске цепочки сначала для каждого твина срабатывал его собственный коллбэк onActive (выводя в консоль "onActive A: 1", "onActive B: 1", "onActive C: 1"). 2. Затем, после создания всех твинов внутри цепочки, срабатывал **общий** коллбэк цепочки onActive (выводя "🔗 onActive Chain 🔗"). 3. **Проблема:** После вызова общего коллбэка цепочки, коллбэк onActive каждого отдельного твина срабатывал **повторно**. В консоли можно было увидеть "onActive A: 2", "onActive B: 2", "onActive C: 2".

Иными словами, функция, переданная в конфиг отдельного твина, вызывалась дважды: один раз при его непосредственном создании внутри цепочки, и второй раз — после активации всей цепочки. Это могло приводить к непреднамеренному дублированию логики (например, двойному списанию ресурсов, двум звукам запуска анимации и т.д.).

Практические выводы и решение

Этот пример — отличная иллюстрация того, почему важно тестировать поведение коллбэков.

* **Проверяйте документацию и актуальность примеров.** Данный код был частью коллекции, демонстрирующей именно некорректное поведение (bugs/). В актуальной стабильной версии Phaser 3 эта проблема уже исправлена. Коллбэк onActive твина в цепочке должен срабатывать ровно один раз при его активации. * **Изолируйте логику в коллбэках.** Если ваша функция onActive меняет критическое состояние игры, предусмотрите защиту от повторного выполнения (флаги, проверки). * **Используйте разные коллбэки для цепочки и для твина.** Как видно из примера, у цепочки (this.tweens.chain) и у отдельного твина есть одноименные события (onActive, onComplete). Четко разделяйте логику: что должно произойти при активации всей последовательности, а что — при старте каждого ее звена.

// Правильное разделение ответственности
this.tweens.chain({
    onActive: function () { console.log('Цепочка началась!'); },
    onComplete: function () { console.log('Вся цепочка завершена!'); },
    tweens: [
        {
            targets: sprite,
            x: 400,
            onActive: function () { console.log('Твин 1 стартовал'); },
            onComplete: function () { console.log('Твин 1 завершился'); }
        }
    ]
});

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

Использование tweens.chain() позволяет создавать сложные, управляемые последовательности анимаций. Ключ к надежности — понимание жизненного цикла событий как всей цепочки, так и каждого твина в ней. Хотя конкретная ошибка с двойным вызовом onActive исправлена, принцип остался важен: всегда проверяйте поведение коллбэков в вашей версии движка. Для экспериментов попробуйте заменить loop: 1 на -1 (бесконечный цикл) и понаблюдайте за порядком вызовов onLoop. Или добавьте коллбэк onComplete на уровне твина и цепочки, чтобы увидеть разницу в моментах их срабатывания.