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

Управление несколькими игровыми состояниями — ключевой навык для создания сложных игр. В Phaser для этого используются сцены (Scenes). Часто возникает необходимость запустить не одну, а сразу несколько активных сцен параллельно, например, для разделения логики игрового мира, интерфейса и спецэффектов. Эта статья на практическом примере покажет, как запускать и управлять параллельными сценами. Вы узнаете, как инициировать несколько сцен по событию, как они взаимодействуют с основным циклом игры и почему такой подход делает код чище и модульнее.

Версия 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.image(400, 300, 'face').setAlpha(0.2);

        const _this = this;

        this.input.once('pointerdown', function ()
        {

            this.scene.launch('sceneB');
            this.scene.launch('sceneC');

        }, this);
    }
}

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

        this.pic;
    }

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

    create ()
    {
        this.pic = this.add.image(400, 300, 'arrow').setOrigin(0, 0.5);
    }

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

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

        this.pic;
    }

    preload ()
    {
        // this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('mech', 'assets/pics/titan-mech.png');
    }

    create ()
    {
        this.pic = this.add.image(Phaser.Math.Between(300, 600), 300, 'mech');
    }

    update (time, delta)
    {
        this.pic.rotation -= 0.02;
    }
}

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

const game = new Phaser.Game(config);

Структура примера: три независимые сцены

В примере определены три класса сцен: SceneA, SceneB и SceneC. Каждая сцена — это самостоятельный модуль игры со своими методами preload, create и update.

Конфигурация игры передает массив всех классов сцен в свойство scene. Phaser автоматически создаст их экземпляры. Важно: первой будет запущена та сцена, которая указана первой в массиве. В нашем случае это SceneA.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: [ SceneA, SceneB, SceneC ] // SceneA запустится первой
};

Запускающая сцена (SceneA) и событие pointerdown

SceneA выступает в роли инициатора. В своем методе create она добавляет полупрозрачное фоновое изображение и устанавливает обработчик события однократного клика (или касания) pointerdown.

Обратите внимание на третий аргумент this в вызове this.input.once. Он задает контекст выполнения для callback-функции. Внутри функции this будет ссылаться на экземпляр SceneA, а не на глобальный объект. Это позволяет нам обращаться к this.scene.

create ()
{
    this.add.image(400, 300, 'face').setAlpha(0.2);

    this.input.once('pointerdown', function ()
    {
        // this здесь — экземпляр SceneA
        this.scene.launch('sceneB');
        this.scene.launch('sceneC');
    }, this); // Контекст передан явно
}

По клику вызывается метод this.scene.launch() для двух других сцен. Это ключевой момент: сцены SceneB и SceneC уже созданы, но не активны. Метод launch активирует их, вызывая их методы create (если они еще не были вызваны) и добавляя их в основной цикл обновления игры.

Параллельное выполнение: методы update в SceneB и SceneC

После запуска SceneB и SceneC становятся активными параллельно с SceneA. Каждая сцена имеет свой собственный метод update, который вызывается на каждом кадре игры.

* В SceneB стрелка (arrow) непрерывно вращается в одну сторону. * В SceneC изображение меха (mech) вращается в противоположную сторону с другой скоростью.

Сцены рендерятся в порядке их запуска (сверху вниз). В этом примере SceneA с фоновым изображением будет отрисована первой, затем SceneB, и поверх всего — SceneC. Это создает эффект композиции.

// update в SceneB
update (time, delta)
{
    this.pic.rotation += 0.01; // Вращение по часовой стрелке
}

// update в SceneC
update (time, delta)
{
    this.pic.rotation -= 0.02; // Вращение против часовой стрелки, в 2 раза быстрее
}

Обе сцены получают одни и те же параметры time и delta, но работают полностью независимо друг от друга, управляя своими собственными игровыми объектами.

Ключевые API для управления сценами

В примере используется минимальный, но достаточный набор методов менеджера сцен (this.scene):

* launch(key): Запускает (активирует) сцену по ее ключу. Если сцена ранее не была создана, она будет создана. Если метод create для этой сцены уже вызывался, он вызван не будет (для повторного запуска с начальными данными используйте restart). * Неявно используется то, что сцены, переданные в конфиг массивом, автоматически создаются (new) при старте игры.

Важно различать создание сцены (конструктор, init, preload) и ее запуск (create, активный update). В этом примере SceneB и SceneC создаются при старте игры, но запускаются и начинают обновляться только после клика.

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

Параллельные сцены — мощный инструмент для декомпозиции игровой логики. Как видно из примера, они позволяют легко разделить фон, анимации и интерфейс на независимые модули, которые можно включать и выключать по необходимости. **Идеи для экспериментов:** 1. Попробуйте запускать сцены SceneB и SceneC не одновременно, а с задержкой, используя this.time.delayedCall. 2. Добавьте в SceneA кнопку, которая будет останавливать (this.scene.stop) одну из параллельных сцен, и посмотрите, как это повлияет на общую картину. 3. Поэкспериментируйте с порядком сцен в массиве конфига и порядком их запуска через launch, чтобы понять, как меняется порядок отрисовки (глубина).