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