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

Любая игра сложнее «прыгни и собери монету» состоит из разных состояний: меню, уровень, пауза, экран результатов. В 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() — это основной инструмент для кардинальной смены игрового состояния, когда предыдущий контекст больше не нужен. Он обеспечивает чистый перезапуск жизненного цикла. Для экспериментов попробуйте

  1. Передавать данные об очках игрока между сценами
  2. Использовать this.scene.launch() для запуска сцены "поверх" текущей (например, сцены паузы)
  3. Остановить одну сцену и запустить несколько других, создав сложную систему UI