О чем этот пример
При разработке игр с анимациями 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-слои.
