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

При разработке игр с анимациями Spine в Phaser можно столкнуться с коварной ошибкой: при переключении между сценами анимации и текстуры могут исчезать или отображаться некорректно. Эта статья разбирает реальный пример бага, возникающего из-за конфликта загрузчиков и плагинов между сценами. Вы узнаете, как правильно инициализировать и передавать ресурсы Spine, чтобы избежать "пожирания" анимаций при старте новой сцены и обеспечить стабильный рендеринг.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor()
    {
        super({
            pack: {
                files: [
                    { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/spine4.1/SpinePluginDebug.js', sceneKey: 'spine1' }
                ]
            },
            key: "example"
        });
    }

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('logo', 'assets/sprites/phaser.png');
        this.load.atlas('atlas', 'assets/atlas/megaset-2.png', 'assets/atlas/megaset-2.json');

        this.load.setPath('assets/spine/4.1/demos/');

        this.load.spine('set1', 'demos.json', [ 'atlas1.atlas', 'atlas2.atlas', 'heroes.atlas' ], true);
    }

    create ()
    {
        // this.cameras.main.setBackgroundColor(0xffffff);
        this.add.image(300,300, "atlas", "frame");
        this.testSpine = this.add.spine(400, 500, 'set1.alien').setScale(0.5);

        this.time.delayedCall(2000, () => 
        {
            // this.testSpine.destroy();
            this.scene.start("test");
        });
    }
}

class Test extends Phaser.Scene
{
    constructor()
    {
        super({
            pack: {
                files: [
                    { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/spine4.1/SpinePluginDebug.js', sceneKey: 'spine2' }
                ]
            },
            key: "test"
        });
    }

    create ()
    {
        this.cameras.main.setBackgroundColor(0x111111);
        
        this.add.image(300,300, "atlas", "frame");
        this.testSpine2 = this.add.spine(400, 500, 'set1.spineboy', 'idle', true);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#1d1d1d',
    parent: 'phaser-example',
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: [ Example, Test ]
};

const game = new Phaser.Game(config);

Корень проблемы: плагины в разных сценах

Ключевая проблема в примере — независимая регистрация плагина Spine для каждой сцены. В конструкторе обеих сцен (Example и Test) плагин SpinePlugin загружается заново с разными sceneKey ('spine1' и 'spine2').

super({
    pack: {
        files: [
            { type: 'scenePlugin', key: 'SpinePlugin', url: 'plugins/spine4.1/SpinePluginDebug.js', sceneKey: 'spine1' }
        ]
    }
});

Каждый плагин создает свой собственный контекст загрузки и управления данными Spine. Когда первая сцена загружает атласы и скелетные данные через this.load.spine, они регистрируются в контексте плагина 'spine1'. При запуске второй сцены (this.scene.start("test")) активируется плагин 'spine2', который не имеет доступа к уже загруженным в 'spine1' данным. Это приводит к ошибке рендеринга или его полному отсутствию.

Анализ сцены Example: загрузка и преждевременный переход

Первая сцена Example выполняет загрузку. Обратите внимание на два важных момента: установка базового URL и изменение пути для загрузки Spine.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.setPath('assets/spine/4.1/demos/');

Метод this.load.spine загружает данные скелетной анимации. Параметр true в конце означает, что анимация будет создана немедленно, используя первый найденный скин.

this.load.spine('set1', 'demos.json', [ 'atlas1.atlas', 'atlas2.atlas', 'heroes.atlas' ], true);

В create добавляется изображение из атласа и Spine-объект 'alien'. Затем, через 2 секунды, происходит жесткий переход на сцену Test.

this.time.delayedCall(2000, () => {
    this.scene.start("test");
});

Этот переход уничтожает текущую сцену и все её объекты, включая контекст плагина 'spine1' с загруженными данными.

Анализ сцены Test: попытка рендеринга в пустоте

Сцена Test пытается создать объект Spine, используя те же ключи данных ('set1.spineboy'), которые были загружены в предыдущей сцене.

this.testSpine2 = this.add.spine(400, 500, 'set1.spineboy', 'idle', true);

Однако плагин 'spine2', активный в этой сцене, не имеет доступа к данным 'set1'. Система загрузки Phaser может кешировать некоторые бинарные данные, но плагин Spine требует, чтобы его собственные структуры данных (скелеты, атласы) были инициализированы в его контексте. Поскольку этого не произошло, объект либо не отрисуется, либо вызовет ошибку в консоли. Изображение из атласа при этом отобразится корректно, так как оно было загружено стандартным загрузчиком Phaser, общим для всех сцен.

Практическое решение: единый плагин и общие сцены

Решение — инициализировать плагин Spine один раз на уровне игры, а не в каждой сцене. В конфигурации игры укажите плагин в массиве plugins.scene. Это сделает его доступным во всех сценах под одним ключом (например, 'spine').

const config = {
    // ... другие настройки ...
    plugins: {
        scene: [
            { key: 'SpinePlugin', plugin: window.SpinePlugin, mapping: 'spine' }
        ]
    }
};

Затем уберите объявление pack из конструкторов сцен. Загрузку ресурсов Spine лучше выполнять в начальной сцене-загрузчике (Boot или Preloader), используя общий для всех сцен плагин. После этого любая сцена сможет создавать объекты через this.add.spine, используя ключ 'spine' для доступа к плагину и его загруженным данным.

Альтернатива: передача данных и осторожный запуск

Если по архитектуре необходимо переключать сцены, но нужно сохранить Spine-объекты, используйте метод this.scene.switch вместо this.scene.start. Метод switch запускает новую сцену, не уничтожая предыдущую.

// Вместо this.scene.start("test")
this.scene.switch("test");

Также можно передать данные в запускаемую сцену через второй аргумент start и вручную перезагрузить ресурсы Spine в контексте её плагина, но это сложный и нерекомендуемый путь. Гораздо надежнее использовать общую загрузку на уровне игры, как описано выше.

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

Конфликты плагинов между сценами — частая причина ошибок рендеринга Spine в Phaser. Для стабильной работы инициализируйте плагин Spine один раз на уровне конфигурации игры. Загружайте все необходимые данные Spine до перехода к игровым сценам. Экспериментируйте: попробуйте загрузить разные наборы Spine-данных в одной сцене или создайте менеджер сцен, который будет управлять Spine-объектами как глобальными актерами, переключая только камеру и UI-слои.