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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class SceneA extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneA', active: true });

        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 SceneB extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneB', active: true });
    }

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

    create ()
    {
        const img = this.add.image(400, 300, 'face');

        this.tweens.add({
            targets: img,
            alpha: 0,
            yoyo: true,
            repeat: -1
        });
    }
}

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

const game = new Phaser.Game(config);

Что такое параллельные сцены?

Обычно в Phaser в любой момент времени активна только одна сцена. Однако, указав параметр active: true в конструкторе, можно сделать несколько сцен активными одновременно. Они будут независимо выполнять свои циклы preload, create и update. Это позволяет разделить ответственность: одна сцена может отвечать за фоновую анимацию, другая — за интерфейс, третья — за основную игровую механику.

В примере две сцены, SceneA и SceneB, объявлены как активные с самого начала. Обратите внимание на порядок их в массиве scene в конфигурации игры: [SceneB, SceneA]. Несмотря на то что SceneB указана первой, SceneA будет запущена первой, потому что порядок в массиве определяет порядок инициализации, но не порядок обновления. Обе сцены получают вызовы update каждый кадр.

Структура сцены с анимацией спрайта

Рассмотрим первую сцену, SceneA. Она загружает одно изображение стрелки и в цикле update непрерывно вращает его.

class SceneA extends Phaser.Scene
{
    constructor ()
    {
        super({ key: 'sceneA', active: true });
        this.pic;
    }

В конструкторе мы передаем объект конфигурации с уникальным ключом key и флагом active: true. Это делает сцену активной сразу после создания. Поле this.pic зарезервировано для ссылки на наш спрайт.

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

В preload устанавливается базовый URL для загрузки и загружается изображение стрелки под ключом 'arrow'.

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

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

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

Функция update вызывается каждый кадр игры. Здесь мы увеличиваем угол вращения (rotation) спрайта на 0.01 радиана, создавая плавную анимацию вращения.

Сцена с твин-анимацией

Вторая сцена, SceneB, демонстрирует другой подход к анимации — через систему твинов Phaser. Она загружает другое изображение и запускает бесконечную анимацию его исчезновения и появления.

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

Конструктор аналогичен первой сцене: та же конфигурация с активным флагом.

preload ()
    {
        this.load.image('face', 'assets/pics/bw-face.png');
    }

Загружается изображение лица. Обратите внимание, что строка setBaseURL закомментирована. Поскольку базовый URL уже был установлен в SceneA, загрузчик Phaser использует это же значение для всех сцен. Если бы мы раскомментировали строку и изменили URL, это повлияло бы на все последующие загрузки.

create ()
    {
        const img = this.add.image(400, 300, 'face');
        this.tweens.add({
            targets: img,
            alpha: 0,
            yoyo: true,
            repeat: -1
        });
    }
}

В create создается изображение лица, и к нему добавляется твин. Конфигурация твина: - targets: объекты для анимации (наше изображение). - alpha: 0: целевое значение прозрачности (полностью невидимый). - yoyo: true: после достижения цели анимация проигрывается в обратном порядке. - repeat: -1: анимация повторяется бесконечно.

Результат — лицо будет плавно исчезать и появляться, накладываясь поверх вращающейся стрелки из SceneA.

Конфигурация игры и запуск

Ключевой момент — регистрация сцен в экземпляре игры. Сцены передаются в виде массива классов в конфигурации.

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

const game = new Phaser.Game(config);

Порядок в массиве scene важен для порядка инициализации. Сначала будет создан и инициализирован экземпляр SceneB, затем SceneA. Однако, поскольку обе сцены активны, их методы update будут вызываться параллельно в каждом кадре. Фоновый цвет backgroundColor применяется ко всему холсту игры и будет виден за всеми сценами.

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

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