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

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