О чем этот пример
Создание масштабных игровых миров с тысячами объектов — классическая проблема производительности. Пример «Changing Large Scene» демонстрирует, как система сцен Phaser 3 помогает управлять ресурсами, переключая между двумя огромными сценами без падения FPS. Эта статья разберет, как устроена загрузка и отрисовка, почему некоторые объекты можно скрывать, и как правильно организовать переход для поддержания плавности геймплея.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class SceneA extends Phaser.Scene {
constructor ()
{
super({ key: 'sceneA' });
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
}
create ()
{
for (var i = 0; i < 5000; i++)
{
var x = Phaser.Math.Between(0, 2500);
var y = Phaser.Math.Between(0, 2500);
var frame = Phaser.Math.Between(0, 399);
var bob = this.add.sprite(x, y, 'bobs', frame);
if (i % 2)
{
// Hide every other sprite, it will still be on the display list though
bob.setVisible(false);
}
}
this.add.text(10, 10, 'Scene A', { font: '16px Courier', fill: '#ffffff' });
this.input.once('pointerup', function () {
this.scene.start('sceneB');
}, this);
}
}
class SceneB extends Phaser.Scene {
constructor ()
{
super({ key: 'sceneB' });
}
create ()
{
for (var i = 0; i < 5000; i++)
{
var x = Phaser.Math.Between(0, 2500);
var y = Phaser.Math.Between(0, 2500);
var frame = Phaser.Math.Between(0, 399);
var bob = this.add.sprite(x, y, 'bobs', frame);
if (i % 2)
{
// Hide every other sprite, it will still be on the display list though
bob.setVisible(false);
}
}
this.add.text(10, 10, 'Scene B', { font: '16px Courier', fill: '#ffffff' });
this.input.once('pointerup', function () {
this.scene.start('sceneA');
}, this);
}
}
var config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB ]
};
var game = new Phaser.Game(config);
Архитектура примера: две тяжелые сцены
Пример построен на двух независимых сценах (SceneA и SceneB), зарегистрированных в конфигурации игры. Это классический паттерн для разделения игровых состояний или уровней.
scene: [ SceneA, SceneB ]
Каждая сцена в своём методе create() порождает 5000 спрайтов, размещая их случайным образом на большом поле размером 2500x2500 пикселей. Это создает нагрузку на рендерер, имитируя условия сложного игрового уровня. Переход между сценами инициируется по клику мыши.
Массовое создание и настройка спрайтов
Ключевая логика работы со спрайтами сосредоточена в цикле for. Для каждого из 5000 объектов генерируются случайные координаты и кадр из спрайтшита.
for (var i = 0; i < 5000; i++) {
var x = Phaser.Math.Between(0, 2500);
var y = Phaser.Math.Between(0, 2500);
var frame = Phaser.Math.Between(0, 399);
var bob = this.add.sprite(x, y, 'bobs', frame);
}
Методы Phaser.Math.Between используются для получения псевдослучайных значений. Важно, что спрайт создается через this.add.sprite, что автоматически добавляет его на дисплейный список и в систему обновления сцены.
Оптимизация через управление видимостью
Прямо в цикле создания применяется простая, но эффективная оптимизация: каждый второй спрайт сразу же скрывается с помощью setVisible(false).
if (i % 2) {
bob.setVisible(false);
}
Это не удаляет спрайт из дисплейного списка — он остается в памяти и его свойства можно изменять. Однако скрытые спрайты не передаются на отрисовку в рендерер (WebGL или Canvas), что существенно снижает нагрузку на GPU. Такой прием полезен для объектов, которые могут понадобиться позже (например, враги за пределами экрана).
Механика переключения сцен
Переход между SceneA и SceneB — это полная остановка одной сцены и запуск другой. Он привязан к однократному событию клика мыши (pointerup).
this.input.once('pointerup', function () {
this.scene.start('sceneB');
}, this);
Метод this.scene.start() останавливает текущую сцену, запускает цепочку её системных событий (shutdown, destroy), а затем запускает целевую сцену (вызывая её preload, create и т.д.). Использование once вместо on гарантирует, что обработчик сработает лишь раз, предотвращая возможные ошибки при повторных кликах.
Что происходит под капотом при старте сцены
Когда вы вызываете this.scene.start('sceneB'), менеджер сцен Phaser выполняет последовательность действий:
1. Останавливает текущую активную сцену (SceneA).
2. Очищает все игровые объекты, созданные в SceneA, и освобождает связанные с ней ресурсы (если они не являются общими, как загруженный спрайтшит).
3. Запускает сцену SceneB, вызывая её метод create (в данном примере preload не вызывается, так как ресурс уже загружен).
4. Все 5000 спрайтов SceneB создаются заново. Несмотря на то, что сцены идентичны по коду, это два независимых набора игровых объектов в памяти.
Что попробовать дальше
Пример наглядно показывает, как Phaser 3 управляет памятью и производительностью через систему сцен. Даже с тысячами объектов переключение происходит плавно, потому что каждая сцена — это изолированное окружение. Для экспериментов попробуйте: увеличить количество спрайтов до предела; не скрывать каждый второй спрайт и сравнить FPS; вынести загрузку спрайтшита в общую область, чтобы избежать повторной загрузки; или использовать this.scene.switch() для более мягкого перехода, если часть объектов можно оставить в памяти.
