О чем этот пример
Флаг `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() будет вызываться только для активных объектов, или реализуйте собственный менеджер, автоматически очищающий твины от уничтоженных целей.
