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

В Phaser управление переключением сцен часто осуществляется в ответ на действия игрока или события. Однако иногда требуется запустить последовательность сцен автоматически, например, для показа вступительных титров или перехода между уровнями без интерактивности. Этот паттерн, когда одна сцена немедленно запускает следующую прямо из своего метода `create()`, позволяет выстроить линейный поток выполнения. В статье разберем практический пример, где пять сцен последовательно запускают друг друга, и только последняя отображает игровой контент. Вы узнаете, как правильно организовывать такие цепочки, в чем их преимущества для разделения логики и какие подводные камни могут возникнуть.

Версия 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');
        this.load.image('arrow', 'assets/sprites/longarrow.png');
    }

    create ()
    {
        console.log('SceneA');

        this.scene.start('sceneB');
    }
}

class SceneB extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneB' });
    }

    create ()
    {
        console.log('SceneB');

        this.scene.start('sceneC');
    }
}

class SceneC extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneC' });
    }

    create ()
    {
        console.log('SceneC');

        this.scene.start('sceneD');
    }
}

class SceneD extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneD' });
    }

    create ()
    {
        console.log('SceneD');

        this.scene.start('sceneE');
    }
}

class SceneE extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneE' });
    }

    create ()
    {
        console.log('SceneE');

        this.add.image(400, 300, 'face');
        this.arrow = this.add.sprite(400, 300, 'arrow').setOrigin(0, 0.5);
    }

    update (time, delta)
    {
        this.arrow.rotation += 0.01;
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: [ SceneA, SceneB, SceneC, SceneD, SceneE ]
};

const game = new Phaser.Game(config);

Создание и регистрация сцен

В Phaser сцена — это основной контейнер для игровой логики, ресурсов и объектов. Каждая сцена должна быть представлена классом, расширяющим Phaser.Scene. Ключевой момент — присвоение уникального ключа (key) в конструкторе, который используется для ссылки на сцену.

Все созданные классы сцен передаются в массив конфигурации игры. Порядок в этом массиве не влияет на порядок их выполнения, управление которым осуществляется программно.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: [ SceneA, SceneB, SceneC, SceneD, SceneE ]
};

const game = new Phaser.Game(config);

Жизненный цикл сцены: от preload к create

Первой всегда запускается сцена, которая указана первой в массиве scene конфигурации — в нашем случае это SceneA. Phaser автоматически вызывает ее методы жизненного цикла.

Метод preload() предназначен для загрузки ресурсов (изображений, звуков, данных). Здесь с помощью this.load.image загружаются две текстуры. Важно: эти ресурсы загружаются в глобальный кэш игры и будут доступны во всех последующих сценах, поэтому нет необходимости загружать их повторно.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('face', 'assets/pics/bw-face.png');
    this.load.image('arrow', 'assets/sprites/longarrow.png');
}

После завершения загрузки Phaser вызывает метод create(). Именно здесь, в SceneA, происходит ключевое действие — немедленный запуск следующей сцены с помощью this.scene.start().

create ()
{
    console.log('SceneA');
    this.scene.start('sceneB');
}

Вызов this.scene.start('sceneB') останавливает текущую сцену (SceneA) и запускает сцену с ключом 'sceneB'. Методы update остановленной сцены больше не вызываются.

Построение линейной цепочки

Принцип, показанный в SceneA, повторяется в следующих трех сценах (`B,C,D). Каждая из них в своем методеcreate()` выводит сообщение в консоль и немедленно запускает следующую сцену. Это создает быструю последовательность переключений.

// Пример для SceneB
create ()
{
    console.log('SceneB');
    this.scene.start('sceneC'); // Запуск SceneC
}

Такая организация кода позволяет четко разделить этапы инициализации или логические состояния игры. Например, SceneA могла бы быть сценой загрузки, SceneB — сценой главного меню, а SceneC — сценой с выбором уровня. В данном примере они пустые и служат только для демонстрации механизма перехода.

Финишная сцена: игра и анимация

Цепочка завершается сценой SceneE. Ее метод create() не запускает новую сцену, а вместо этого создает игровые объекты — изображение (this.add.image) и спрайт (this.add.sprite). Это переводит игру в активное состояние.

create ()
{
    console.log('SceneE');
    this.add.image(400, 300, 'face');
    this.arrow = this.add.sprite(400, 300, 'arrow').setOrigin(0, 0.5);
}

Метод setOrigin(0, 0.5) устанавливает точку вращения спрайта стрелки на ее левом краю по центру по вертикали, что необходимо для корректного вращения вокруг этого края.

Поскольку сцена больше не переключается, Phaser начинает вызывать ее метод update() каждый кадр. Здесь это используется для простой анимации вращения стрелки.

update (time, delta)
{
    this.arrow.rotation += 0.01;
}

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

Паттерн последовательного запуска сцен из метода create() — мощный инструмент для организации линейного потока в игре, такого как кат-сцены, туториалы или сложная многоэтапная инициализация. Он обеспечивает чистоту кода, изолируя логику каждого этапа в отдельной сцене. Для экспериментов попробуйте: 1. Добавить задержку между переходами, используя this.time.delayedCall. 2. Загружать уникальные ресурсы в preload() каждой промежуточной сцены. 3. Возвращаться по цепочке назад, используя this.scene.start('sceneA') из SceneE по нажатию клавиши.