О чем этот пример
Работа с таймлайнами (`Timeline`) в Phaser 3 — мощный инструмент для создания сложных последовательностей анимаций. Однако их преждевременное или повторное уничтожение может привести к трудноотлавливаемым ошибкам в консоли. Эта статья разбирает пример из официального репозитория, демонстрирующий проблему, и объясняет, как безопасно управлять жизненным циклом таймлайнов, чтобы ваш код оставался стабильным.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
timelines = []
preload ()
{
// this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
this.load.atlas('timeline', 'https://labs.phaser.io/assets/atlas/timeline.png', 'https://labs.phaser.io/assets/atlas/timeline.json');
}
create ()
{
const timeline = this.createTimeline();
timeline.play()
setTimeout(() => {if (timeline.isPlaying()) timeline.destroy()},100)
setTimeout(() => {if (timeline.isPlaying()) timeline.destroy()},2300) // this causes the error
}
createTimeline(){
return this.add.timeline([
{
at: 1000,
tween: {
targets: this.add.sprite(400, 700, 'timeline', 'tombstone'),
y: 400,
duration: 1000,
ease: 'Power2'
}
}
]);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#020286',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
В чём проблема?
Исходный код создаёт простую сцену с таймлайном, который перемещает спрайт. Ключевая проблема кроется в блоке create, где дважды, с разной задержкой, вызывается метод timeline.destroy().
setTimeout(() => {if (timeline.isPlaying()) timeline.destroy()},100)
setTimeout(() => {if (timeline.isPlaying()) timeline.destroy()},2300) // this causes the error
Первый вызов destroy() (через 100 мс) выполняется успешно и останавливает таймлайн. Второй вызов (через 2300 мс) пытается уничтожить уже несуществующий объект, что приводит к ошибке в консоли. Проверка timeline.isPlaying() не спасает, так как после уничтожения обращение к методу объекта тоже может вызвать проблемы.
Как работает Timeline и его уничтожение
Таймлайн в Phaser — это контейнер для твинов, которые выполняются в заданной последовательности. При создании через this.add.timeline() он автоматически добавляется в систему обновления сцены.
return this.add.timeline([
{
at: 1000,
tween: {
targets: this.add.sprite(400, 700, 'timeline', 'tombstone'),
y: 400,
duration: 1000,
ease: 'Power2'
}
}
]);
Метод destroy() останавливает все твины внутри таймлайна, отключает его слушатели событий и удаляет из систем сцены. **Важно:** после вызова destroy() объект больше не должен использоваться. Попытка вызвать любой его метод (включая isPlaying() или повторный destroy()) приведёт к ошибке, так как внутренние ссылки обнулены.
Практическое решение: отслеживание состояния
Чтобы избежать ошибок, необходимо явно отслеживать состояние таймлайна. Самый надёжный способ — обнулять ссылку на объект после его уничтожения.
create ()
{
this.timeline = this.createTimeline(); // Сохраняем ссылку в контексте сцены
this.timeline.play();
setTimeout(() => {
if (this.timeline) {
this.timeline.destroy();
this.timeline = null; // Критически важный шаг
}
}, 100);
setTimeout(() => {
// Теперь проверка работает корректно
if (this.timeline) {
this.timeline.destroy();
this.timeline = null;
}
}, 2300);
}
Проверка if (this.timeline) гарантирует, что мы не пытаемся взаимодействовать с уничтоженным объектом. Обнуление ссылки (this.timeline = null) делает это состояние явным.
Альтернатива: использование событий Timeline
Более идиоматичный для Phaser подход — использование встроенных событий таймлайна. Вы можете уничтожить его по завершению всех анимаций, что исключает необходимость вручную рассчитывать таймеры.
create ()
{
const timeline = this.createTimeline();
// Уничтожаем таймлайн после его полного завершения
timeline.on('complete', () => {
timeline.destroy();
});
timeline.play();
// Или, если нужно уничтожить досрочно, можно использовать событие 'stop'
// timeline.on('stop', () => { timeline.destroy(); });
}
Этот способ более безопасен и лучше интегрируется в событийную модель фреймворка. Событие 'complete' сработает, когда все твины в цепочке завершатся.
Что попробовать дальше
Уничтожение таймлайнов требует аккуратности: всегда обнуляйте ссылки на объект после вызова destroy() или используйте событийную модель Phaser. Для экспериментов попробуйте создать цепочку из нескольких таймлайнов с перекрестными вызовами и отработайте их безопасное удаление в разных условиях (по событию, по клику, при смене сцены). Это поможет построить устойчивую архитектуру для сложных последовательностей анимации.
