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

Работа с анимациями в 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) объектов твинов для оптимизации производительности при частом пересоздании.