О чем этот пример
В сложных играх часто требуется разделять логику: одна сцена управляет геймплеем, другая — интерфейсом. Но как им обмениваться данными? Встроенная система событий Phaser позволяет сценам взаимодействовать без прямых ссылок, сохраняя архитектуру чистой и модульной. Этот подход упрощает тестирование, отладку и масштабирование проекта. В статье разберем рабочий пример с двумя сценами — игровой и UI. Вы научитесь создавать пользовательские события, передавать их между сценами и обновлять интерфейс в реальном времени. Это фундамент для построения сложных игровых систем, таких как счетчики очков, панели здоровья или квестовые журналы.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class SceneA extends Phaser.Scene {
constructor ()
{
super('GameScene');
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/skies/sky4.png');
this.load.image('crate', 'assets/sprites/crate.png');
}
create ()
{
this.add.image(400, 300, 'bg');
for (let i = 0; i < 64; i++)
{
let x = Phaser.Math.Between(0, 800);
let y = Phaser.Math.Between(0, 600);
this.add.image(x, y, 'crate').setInteractive();
}
this.input.on('gameobjectup', this.clickHandler, this);
}
clickHandler (pointer, box)
{
// Disable our box
box.input.enabled = false;
box.setVisible(false);
// Dispatch a Scene event
this.events.emit('addScore');
}
}
class SceneB extends Phaser.Scene {
constructor ()
{
super({ key: 'UIScene', active: true });
this.score = 0;
}
create ()
{
// Our Text object to display the Score
let info = this.add.text(10, 10, 'Score: 0', { font: '48px Arial', fill: '#000000' });
// Grab a reference to the Game Scene
let ourGame = this.scene.get('GameScene');
// Listen for events from it
ourGame.events.on('addScore', function () {
this.score += 10;
info.setText('Score: ' + this.score);
}, this);
}
}
let config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB ]
};
let game = new Phaser.Game(config);
Структура примера: две сцены, одна конфигурация
В примере определены две сцены ES6-классами: SceneA (игровая) и SceneB (UI). Они передаются в массив scene конфигурации движка. Ключевой момент — порядок: Phaser создаст все сцены, но active: true в конструкторе SceneB гарантирует, что UI-сцена будет активна сразу, поверх игровой.
let config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: [ SceneA, SceneB ]
};
Класс SceneA зарегистрирован с ключом 'GameScene' в конструкторе. Это его системное имя, по которому другие сцены смогут найти его через this.scene.get().
Игровая сцена: создание объектов и кликов
В SceneA метод create() заполняет экран 64 интерактивными ящиками (crate). Каждый ящик получает setInteractive(), что делает его чувствительным к вводу. Затем сцена подписывается на глобальное событие gameobjectup с помощью this.input.on. Это событие генерируется движком при отпускании кнопки мыши над любым интерактивным игровым объектом.
this.add.image(x, y, 'crate').setInteractive();
this.input.on('gameobjectup', this.clickHandler, this);
Обработчик clickHandler получает ссылку на нажатый ящик (box). Он отключает его интерактивность и скрывает, имитируя «сбор» объекта. Затем происходит самое важное — генерация пользовательского события:
this.events.emit('addScore');
Объект this.events — это диспетчер событий, встроенный в каждую сцену Phaser. Метод emit рассылает событие с именем 'addScore' всем его подписчикам.
UI-сцена: подписка на события и обновление текста
SceneB создает текстовый объект для отображения счета. Чтобы получать уведомления из игровой сцены, ей сначала нужно получить на нее ссылку. Это делается через менеджер сцен:
let ourGame = this.scene.get('GameScene');
Затем UI-сцена подписывается на событие 'addScore', которое испускает SceneA. Обратите внимание на третий аргумент this в on() — он задает контекст выполнения колбэка, чтобы внутри функции this.score ссылался на экземпляр SceneB.
ourGame.events.on('addScore', function () {
this.score += 10;
info.setText('Score: ' + this.score);
}, this);
Каждый вызов emit('addScore') в игровой сцене увеличивает счет на 10 и обновляет текст на экране. Связь происходит без жесткой зависимости — UI-сцена знает только имя ('GameScene') и имя события.
Почему это работает: паттерн Наблюдатель в Phaser
В основе лежит паттерн «Наблюдатель» (Observer). Игровая сцена (SceneA) — это издатель (publisher), который оповещает о событии, не зная, кто его получит. UI-сцена (SceneB) — подписчик (subscriber), который реагирует на изменение. Система событий Phaser (this.events) выступает в роли шины сообщений.
Такое разделение ответственности критично для UI: интерфейс должен только отображать состояние, а не управлять игровыми объектами. Если позже вы захотите добавить звук при сборе ящика, вы просто создадите третью сцену (аудио-менеджер) и подпишете ее на то же событие addScore, не меняя код SceneA или SceneB.
Важные нюансы и типичные ошибки
1. **Контекст this**: Если в колбэке on() не указать контекст, this будет ссылаться на диспетчер событий (ourGame.events), а не на вашу сцену. Это вызовет ошибку при обращении к this.score.
2. **Время жизни сцены**: Подписку лучше делать в create() или позже, когда сцена гарантированно инициализирована. Не стоит подписываться в init() или конструкторе, так как другая сцена может еще не существовать.
3. **Утечка памяти**: Если сцена будет перезапущена или уничтожена, подписки не удалятся автоматически. Используйте events.off() или shutdown() для очистки.
4. **Передача данных**: Событие emit может передавать аргументы. Например, можно отправлять не просто факт клика, а количество очков за ящик:
// В SceneA
this.events.emit('addScore', 15);
// В SceneB
ourGame.events.on('addScore', function (points) {
this.score += points;
}, this);
Что попробовать дальше
События — это гибкий и мощный механизм коммуникации между сценами в Phaser. Они позволяют создавать модульную архитектуру, где каждая сцена отвечает за свою зону ответственности. Для экспериментов попробуйте
- Добавить третью сцену, которая воспроизводит звук при событии
addScore - Передавать разные количества очков в зависимости от типа собранного объекта
- Реализовать систему достижений, которая срабатывает при определенном счете, подписавшись на обновление текста в UI-сцене
