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

При работе с анимациями в Phaser иногда возникает необходимость принудительно остановить твины. Однако неправильное использование методов `tweens.killTweensOf()` или `tweens.killAll()` в обработчиках событий завершения твина может привести к ошибкам и неожиданному поведению. Эта статья на практическом примере разберет причину ошибки и покажет, как безопасно управлять жизненным циклом твинов.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
    }

    create ()
    {
        const i = this.add.rectangle(500, 500, 100, 100, 0xff0000);

        // doing it this way doesn't cause the error
        // this.add.tween({ targets: i, duration: 1000, props: { scale: 0.5 }, onComplete: () => this.tweens.killTweensOf(i) });

        // doing it this way does cause the error
        // this.add.tween({ targets: i, duration: 1000, props: { scale: 0.5 } }).once(Phaser.Tweens.Events.TWEEN_COMPLETE, () => this.tweens.killTweensOf(i));
        this.add.tween({ targets: i, duration: 1000, props: { scale: 0.5 } }).once(Phaser.Tweens.Events.TWEEN_COMPLETE, () => this.tweens.killAll());
    }
}

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

const game = new Phaser.Game(config);

Суть проблемы

В предоставленном примере создается твин для масштабирования прямоугольника. В его обработчике события TWEEN_COMPLETE вызывается метод this.tweens.killAll(). Этот вызов приводит к ошибке.

Ключевая причина в том, что метод killAll() (и аналогично killTweensOf()) не просто помечает твин для удаления, а немедленно очищает внутренние структуры данных менеджера твинов. Если этот вызов происходит *во время* обработки события о завершении этого же твина, система пытается обратиться к уже удаленным данным, что вызывает сбой.

Это классическая проблема порядка выполнения и очистки ресурсов.

Анализ ошибочного кода

Давайте посмотрим на код, который вызывает проблему. Твин создается через фабрику this.add.tween(). Сразу к нему цепляется обработчик события завершения.

this.add.tween({
    targets: i,
    duration: 1000,
    props: { scale: 0.5 }
}).once(Phaser.Tweens.Events.TWEEN_COMPLETE, () => this.tweens.killAll());

Здесь цепочка событий следующая: 1. Твин завершает свою анимацию. 2. Срабатывает внутреннее событие TWEEN_COMPLETE. 3. Вызывается наша стрелочная функция, которая выполняет this.tweens.killAll(). 4. Метод killAll() уничтожает все твины, включая только что завершившийся, прямо в процессе его финализации. 5. Система твинов, ожидающая, что структура данных останется валидной до конца обработки события, ломается.

Аналогичная ошибка произойдет и с this.tweens.killTweensOf(i), так как она тоже удаляет конкретный твин в небезопасный момент.

Безопасная альтернатива: onComplete

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

this.add.tween({
    targets: i,
    duration: 1000,
    props: { scale: 0.5 },
    onComplete: () => {
        // Этот код выполняется безопасно, после полной обработки завершения твина.
        this.tweens.killAll(); // Теперь это не вызовет ошибку.
        // или this.tweens.killTweensOf(i);
    }
});

Использование onComplete является предпочтительным и идиоматичным способом для Phaser, когда логика привязана к завершению конкретного твина.

Когда использовать killTweensOf и killAll

Эти методы мощные, но требуют аккуратного применения. Их основное назначение — экстренная или принудительная остановка анимаций.

*   `this.tweens.killTweensOf(target)`: Останавливает все твины, воздействующие на конкретный игровой объект (цель). Полезно, например, когда игрок прерывает анимацию движения персонажа новым действием.
*   `this.tweens.killAll()`: Полностью очищает менеджер твинов. Часто используется при перезагрузке уровня или сцены, чтобы гарантированно убрать все висящие анимации.

Важное правило: **не вызывайте эти методы из обработчиков событий, связанных с жизненным циклом твина (TWEEN_COMPLETE, TWEEN_UPDATE и т.д.)**. Вместо этого планируйте их вызов в других местах игровой логики — по внешнему событию (клик, таймер) или в методах сцены, таких как shutdown().

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

Ошибка «kill tweens error» — это наглядный пример того, как важно понимать порядок выполнения кода в рамках игрового движка. Главный вывод: для логики, которая должна выполниться по завершении твина, всегда используйте параметр конфигурации onComplete. Методы принудительной остановки killTweensOf и killAll оставьте для реакций на внешние события или очистки сцены. **Идеи для экспериментов:** 1. Создайте несколько твинов для разных объектов и попробуйте остановить их все по нажатию клавиши, используя killAll() в обработчике this.input.keyboard.on('keydown'). 2. Реализуйте систему, где новый твин для объекта автоматически отменяет предыдущий, используя killTweensOf(target) перед созданием нового анимационного эффекта. 3. Проверьте работу killTweensOf на объекте, который является целью сразу для нескольких твинов (например, одновременно двигается и меняет прозрачность).