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

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

  1. Добавить третью сцену, которая воспроизводит звук при событии addScore
  2. Передавать разные количества очков в зависимости от типа собранного объекта
  3. Реализовать систему достижений, которая срабатывает при определенном счете, подписавшись на обновление текста в UI-сцене