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

Флаг `persist` в твинах Phaser 3 — мощный инструмент для создания циклических анимаций, который позволяет твину продолжать существовать после завершения. Однако его использование в связке с уничтоженными игровыми объектами может привести к трудноуловимым ошибкам и падению производительности. Эта статья разбирает пример кода из репозитория Phaser, демонстрирующий потенциальную проблему, и объясняет, как правильно управлять жизненным циклом твинов, чтобы ваша игра оставалась стабильной.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    create ()
    {
        this.objToTween1 = this.add.circle(this.scale.width / 4, this.scale.height / 2, 50, 0xff0000);

        const persistTweenWithoutCompleteDelay = this.add.tween({ targets: this.objToTween1, duration: 500, props: { scale: 0 }, yoyo: true, persist: true });
        persistTweenWithoutCompleteDelay.name = 'noDelay';

        persistTweenWithoutCompleteDelay.on(Phaser.Tweens.Events.TWEEN_COMPLETE, () =>
        {
            this.time.delayedCall(500, () =>
            {
                persistTweenWithoutCompleteDelay.play();
            });
        });

        this.objToTween2 = this.add.circle(this.scale.width / 4 * 3, this.scale.height / 2, 50, 0xff0000);

        const persistTweenWithCompleteDelay = this.add.tween({ targets: this.objToTween2, duration: 500, props: { scale: 0 }, yoyo: true, persist: true, completeDelay: 500 });
        persistTweenWithCompleteDelay.name = 'withDelay';

        persistTweenWithCompleteDelay.on(Phaser.Tweens.Events.TWEEN_COMPLETE, () =>
        {
            this.time.delayedCall(500, () =>
            {
                persistTweenWithCompleteDelay.play();
            });
        });
    }
}

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

const game = new Phaser.Game(config);

Суть проблемы: твин переживает свой target

Ключевая проблема возникает, когда игровой объект, являющийся целью твина (target), уничтожается (например, методом destroy()), а сам твин продолжает существовать благодаря флагу persist: true. В примере создаются два круга и для каждого — повторяющийся твин, уменьшающий и возвращающий масштаб.

Твин с persist: true не удаляется автоматически из менеджера твинов сцены после завершения. Это позволяет вручную перезапускать его позже, как это делается в обработчике события TWEEN_COMPLETE. Но если уничтожить круг, например, this.objToTween1.destroy(), твин persistTweenWithoutCompleteDelay останется в памяти и будет пытаться анимировать несуществующий объект, что может вызвать ошибки или бесполезную трату ресурсов.

Разбор примера: два похожих, но разных твина

В коде создаются два практически идентичных твина. Их основное отличие — наличие параметра completeDelay у второго.

const persistTweenWithoutCompleteDelay = this.add.tween({
    targets: this.objToTween1,
    duration: 500,
    props: { scale: 0 },
    yoyo: true,
    persist: true // Твин не будет удален автоматически
});
const persistTweenWithCompleteDelay = this.add.tween({
    targets: this.objToTween2,
    duration: 500,
    props: { scale: 0 },
    yoyo: true,
    persist: true,
    completeDelay: 500 // Задержка перед вызовом TWEEN_COMPLETE
});

Оба твина настроены на бесконечный цикл через обработчик события TWEEN_COMPLETE, который с задержкой в 500 мс перезапускает твин методом play().

Событие TWEEN_COMPLETE и жизненный цикл

Событие Phaser.Tweens.Events.TWEEN_COMPLETE — это важный хук для управления анимациями. В примере оно используется для создания цикла.

persistTweenWithCompleteDelay.on(Phaser.Tweens.Events.TWEEN_COMPLETE, () => {
    this.time.delayedCall(500, () => {
        persistTweenWithCompleteDelay.play(); // Перезапуск твина
    });
});

Важно понимать, что при наличии completeDelay событие сработает только после этой задержки. Если уничтожить целевой объект *до* срабатывания события, твин все равно попытается выполнить код в обработчике и перезапуститься, что приведет к проблемам.

Правила безопасной работы с persist-твинами

Чтобы избежать ошибок, соблюдайте следующие практики:

1. **Всегда останавливайте и удаляйте твин при уничтожении его цели.** Вызовите tween.stop() и tween.remove() (или tween.destroy()), прежде чем уничтожать игровой объект. 2. **Используйте обработчики событий сцены.** В событии scene.shutdown или перед перезапуском сцены обязательно очищайте все персистентные твины. 3. **Проверяйте существование target.** При ручном перезапуске твина в коллбэке можно добавить проверку if (target.scene), чтобы убедиться, что объект все еще находится в активной сцене.

Пример безопасной остановки:

// При уничтожении объекта
myObject.destroy();
if (myPersistentTween) {
    myPersistentTween.stop();
    myPersistentTween.remove(); // Удаляет твин из менеджера
}

Почему completeDelay не решает проблему

Параметр completeDelay добавляет паузу между окончанием анимации и срабатыванием события TWEEN_COMPLETE. В контексте проблемы persist он не является решением, а лишь меняет временной промежуток, в течение которого может возникнуть конфликт.

completeDelay: 500 // Проблема не исчезнет, просто отсрочится

Если объект будет уничтожен в течение этих 500 мс задержки, твин все равно "оживет" по таймеру и попытается работать с разрушенной целью. Базовое правило остается неизменным: жизненный цикл твина должен быть явно привязан к жизненному циклу его цели.

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

Флаг persist — это ответственность. Он передает управление жизненным циклом твина из рук движка в руки разработчика. Чтобы избежать утечек памяти и ошибок, всегда явно останавливайте и удаляйте такие твины при уничтожении их целей или деактивации сцены. Для экспериментов попробуйте создать систему пула объектов с твинами, где перезапуск play() будет вызываться только для активных объектов, или реализуйте собственный менеджер, автоматически очищающий твины от уничтоженных целей.