О чем этот пример
Работа с анимациями в Phaser через систему Tween обычно интуитивна и надежна. Однако в определенных условиях штатный метод `restart()` может вести себя непредсказуемо, приводя к "зависанию" твина. Этот пример наглядно демонстрирует тонкий баг, возникающий при попытке перезапустить твин, находящийся в специфическом внутреннем состоянии `PENDING_REMOVE`. Понимание этой проблемы поможет вам избежать скрытых ошибок в логике игровых анимаций и написать более устойчивый код.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
new Phaser.Game({
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: {
preload: preload,
create: create,
update: update
}
});
var text, arrow, tween;
function preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('arrow', 'assets/sprites/arrow.png');
}
function create ()
{
text = this.add.text(30, 20, '0', { font: '16px Courier', fill: '#00ff00' });
this.add.image(700, 300, 'arrow').setAlpha(0.5);
arrow = this.add.image(100, 300, 'arrow');
tween = this.tweens.add({
targets: arrow,
x: 700,
ease: 'Linear',
duration: 1000
});
setInterval(() => {
if (tween.state === 25)
{
console.log('pending_remove');
tween.restart();
console.log(tween.state);
}
else
{
tween.restart();
}
},
1000);
}
function update ()
{
text.setText('Progress : ' + tween.progress + '\nState : ' + tween.state);
}
Суть проблемы: Состояние PENDING_REMOVE
Каждый твин в Phaser имеет внутреннее числовое состояние (state). В примере код проверяет, равно ли состояние числу 25, которое соответствует константе PENDING_REMOVE. Это состояние означает, что твин завершил свою работу (достиг цели), и система анимаций помечает его для удаления из активного списка в следующем цикле обновления.
Проблема возникает именно в этот короткий промежуток времени. Если вызвать метод restart() для твина в состоянии PENDING_REMOVE, его внутреннее состояние может сброситься не полностью или непредсказуемо, что приводит к тому, что твин перестает обновлять свои свойства (например, позицию `x` спрайта), хотя его состояние в коде может измениться. Спрайт "зависает" на месте.
Разбор кода примера
В функции create() создается базовая сцена и твин, который двигает стрелку от x=100 до x=700 за 1 секунду.
tween = this.tweens.add({
targets: arrow,
x: 700,
ease: 'Linear',
duration: 1000
});
Затем, с помощью setInterval, каждую секунду предпринимается попытка перезапустить этот твин. Логика пытается обработать особый случай состояния PENDING_REMOVE (25), но это не решает проблему.
setInterval(() => {
if (tween.state === 25) {
console.log('pending_remove');
tween.restart();
console.log(tween.state);
} else {
tween.restart();
}
}, 1000);
Функция update() просто выводит текущий прогресс и состояние твина на экран для наглядности.
Почему restart() ломается?
Метод restart() предназначен для сброса твина к начальным значениям и его повторного запуска. Однако его внутренняя реализация может некорректно обрабатывать сценарий, когда твин уже выведен из основного цикла выполнения и помечен на удаление (PENDING_REMOVE). В этом состоянии часть внутренних флагов или ссылок может быть уже очищена, и вызов restart() не восстанавливает твин до полностью работоспособного состояния. Он может формально сменить состояние (например, на ACTIVE), но механизм обновления значений (`x,y` и т.д.) больше не работает.
Практическое решение: Создание нового твина
Самый надежный способ избежать этой проблемы — не переиспользовать твин, который мог завершиться. Вместо вызова restart() для потенциально "битого" объекта, создайте новый твин с теми же параметрами.
Вместо setInterval с проверкой состояния и вызовом restart(), можно использовать событие onComplete у твина для его безопасного перезапуска через создание нового экземпляра.
function createNewTween() {
// Удаляем старый твин, если он есть
if (tween) {
tween.remove();
}
// Создаем новый с нуля
tween = this.tweens.add({
targets: arrow,
x: 700,
ease: 'Linear',
duration: 1000,
onComplete: () => {
// Через секунду после завершения создаем следующий твин
this.time.delayedCall(1000, createNewTween, [], this);
}
});
}
// Первоначальный запуск
createNewTween.call(this);
Этот подход гарантирует, что вы всегда работаете с "свежим" и полностью функциональным объектом твина.
Что попробовать дальше
Баг с методом tween.restart() в состоянии PENDING_REMOVE — это пример тонкого краевого случая в API Phaser. Надежнее всего избегать частого использования restart() для длительных или циклических анимаций, предпочитая создание новых твинов. Для экспериментов попробуйте воспроизвести баг с другими свойствами (например, вращением angle), использовать разные ease-функции или реализовать пул (pool) объектов твинов для оптимизации производительности при частом пересоздании.
