О чем этот пример
Любая игра сложнее «прыгни и собери монету» состоит из разных состояний: меню, уровень, пауза, экран результатов. В Phaser 3 базовой единицей для организации такого кода является сцена (Scene). Умение грамотно запускать и останавливать сцены — ключ к чистой архитектуре и управляемому игровому процессу. В этой статье на примере простого цикла из трёх сцен мы разберём, как работает `this.scene.start()` и что происходит «под капотом» при переключении.
Версия 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.image('face', 'assets/pics/bw-face.png');
}
create ()
{
this.add.sprite(400, 300, 'face').setAlpha(0.2);
this.input.once('pointerdown', function ()
{
console.log('From SceneA to SceneB');
this.scene.start('sceneB');
}, this);
}
}
class SceneB extends Phaser.Scene
{
constructor ()
{
super({ key: 'sceneB' });
}
preload ()
{
// this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('arrow', 'assets/sprites/longarrow.png');
}
create ()
{
this.arrow = this.add.sprite(400, 300, 'arrow').setOrigin(0, 0.5);
this.input.once('pointerdown', function (event)
{
console.log('From SceneB to SceneC');
this.scene.start('sceneC');
}, this);
}
update (time, delta)
{
this.arrow.rotation += 0.01;
}
}
class SceneC extends Phaser.Scene
{
constructor ()
{
super({ key: 'sceneC' });
}
preload ()
{
// this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('mech', 'assets/pics/titan-mech.png');
}
create ()
{
this.add.sprite(Phaser.Math.Between(0, 800), 300, 'mech');
this.input.once('pointerdown', function (event)
{
console.log('From SceneC to SceneA');
this.scene.start('sceneA');
}, this);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB, SceneC ]
};
const game = new Phaser.Game(config);
Основы: создание и регистрация сцен
Сцена в Phaser — это класс, расширяющий Phaser.Scene. Его ключевая особенность — система хуков жизненного цикла: preload(), create(), update().
Каждая сцена должна иметь уникальный строковый ключ, который передаётся в конструктор родительского класса. Этот ключ используется системой для идентификации сцены.
Все созданные классы сцен необходимо передать в конфигурацию игры в массиве scene. Порядок в массиве может влиять на то, какая сцена будет запущена первой, если не указано иное.
class SceneA extends Phaser.Scene {
constructor () {
// Уникальный ключ 'sceneA' для идентификации
super({ key: 'sceneA' });
}
}
const config = {
// ... другие настройки ...
// Регистрируем все сцены в игре
scene: [ SceneA, SceneB, SceneC ]
};
Запуск новой сцены: метод start()
Для перехода от одной активной сцены к другой используется метод this.scene.start(). Этот метод выполняет несколько важных действий:
1. **Останавливает** текущую активную сцену (вызывая её методы shutdown() и destroy()).
2. **Запускает** целевую сцену по её ключу, начиная её жизненный цикл с метода init() (если он есть), затем preload() и create().
3. Делает целевую сцену **активной**.
В нашем примере переход инициируется по клику мыши. Обратите внимание на использование this.input.once(), чтобы обработчик сработал только один раз.
// Внутри SceneA.create()
this.input.once('pointerdown', function () {
console.log('From SceneA to SceneB');
// Останавливаем SceneA, запускаем SceneB
this.scene.start('sceneB');
}, this);
Важно передать контекст this в качестве последнего аргумента в once(), иначе внутри функции-коллбэка this будет ссылаться не на экземпляр сцены, и вызов this.scene.start() завершится ошибкой.
Жизненный цикл сцены в действии
Давайте проследим, что происходит при переходе из SceneA в SceneB.
1. **SceneA** загрузила изображение `face` в `preload()` и отобразила его в `create()`.
2. После клика вызывается `this.scene.start('sceneB')`.
3. **SceneA** останавливается и уничтожается. Её ресурсы (например, загруженное изображение `face`) обычно остаются в кэше загрузчика игры.
4. Начинается работа **SceneB**. Сначала выполняется её метод `preload()` для загрузки нового ресурса `arrow`.
5. Затем выполняется `create()`, где спрайт `arrow` создаётся и сохраняется в свойство `this.arrow` для дальнейшего использования.
6. Так как сцена теперь активна, игровой цикл начинает каждый кадр вызывать её метод `update()`, в котором стрелка вращается.
// SceneB: update() вызывается каждый кадр после create()
update (time, delta) {
// Поворачиваем спрайт, сохранённый в this.arrow
this.arrow.rotation += 0.01;
}
Управление данными и состоянием между сценами
Метод start() может принимать данные для передачи в запускаемую сцену. Это второй (опциональный) аргумент метода.
// При запуске сцены передаём ей объект с данными
this.scene.start('sceneB', { playerScore: 100, level: 5 });
Принятые данные будут доступны в методе init() или create() целевой сцены в качестве первого аргумента.
class SceneB extends Phaser.Scene {
init(data) {
// Получаем данные, переданные при старте
console.log(data.playerScore); // 100
this.finalScore = data.playerScore;
}
}
В рассмотренном примере данные не передаются, и каждая сцена работает как независимый модуль, образуя бесконечный цикл A -> B -> C -> A.
Что попробовать дальше
Метод this.scene.start() — это основной инструмент для кардинальной смены игрового состояния, когда предыдущий контекст больше не нужен. Он обеспечивает чистый перезапуск жизненного цикла. Для экспериментов попробуйте
- Передавать данные об очках игрока между сценами
- Использовать
this.scene.launch()для запуска сцены "поверх" текущей (например, сцены паузы) - Остановить одну сцену и запустить несколько других, создав сложную систему UI
