О чем этот пример
Работа с анимациями через `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 на уровне твина и цепочки, чтобы увидеть разницу в моментах их срабатывания.
