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

При разработке игр часто возникает необходимость разделить логику: например, интерфейс (UI) и игровой мир должны работать независимо. Использование нескольких сцен в Phaser 3 — мощный инструмент для такой организации. Однако с событиями ввода, такими как перетаскивание объектов, это может привести к неочевидным конфликтам. Этот пример наглядно показывает, как две сцены, каждая со своими интерактивными объектами, сосуществуют, и как управлять их поведением, чтобы они не мешали друг другу. Понимание этого механизма критически важно для создания сложных интерфейсов и игр с наложением HUD.

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('box', 'assets/sprites/128x128-v2.png');
    }

    create ()
    {
        // this.input.setGlobalTopOnly(true);

        const box = this.add.image(400, 300, 'box').setInteractive();

        box.on('pointerdown', () =>
        {

            box.tint = Math.random() * 0xffffff;

        });
    }
}

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

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

    create ()
    {
        const image = this.add.sprite(200, 300, 'eye').setInteractive();

        this.input.setDraggable(image);

        this.input.on('dragstart', (pointer, gameObject) =>
        {

            gameObject.setTint(0xff0000);

        });

        this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
        {

            gameObject.x = dragX;
            gameObject.y = dragY;

        });

        this.input.on('dragend', (pointer, gameObject) =>
        {

            gameObject.clearTint();

        });
    }
}

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

const game = new Phaser.Game(config);

Анатомия примера: две независимые сцены

В примере создаются и запускаются одновременно две сцены: GameScene и UIScene. Они загружают разные изображения и настраивают свою собственную логику взаимодействия.

Ключевой момент — обе сцены активны (active: true в конструкторе UIScene подразумевает, что она запущена сразу, как и GameScene). По умолчанию события ввода (клики, драг) будут обрабатываться всеми активными сценами, если объект в них интерактивен. Это может привести к ситуации, когда клик по объекту UI также вызовет реакцию в игровой сцене, если их области пересекаются.

scene: [ GameScene, UIScene ]
super({ key: 'UIScene', active: true });

Игровая сцена (GameScene): Простое взаимодействие

GameScene создает один спрайт-коробку в центре экрана и делает его интерактивным с помощью setInteractive(). На событие pointerdown (нажатие кнопки мыши или касание) она реагирует простым изменением цвета (тинта).

Эта сцена не использует систему перетаскивания Phaser. Она просто слушает событие нажатия. Если бы сцена UI не перехватывала ввод, клик по перетаскиваемому глазу также менял бы цвет коробки, так как обе сцены получили бы одно и то же событие.

const box = this.add.image(400, 300, 'box').setInteractive();
box.on('pointerdown', () => {
    box.tint = Math.random() * 0xffffff;
});

Сцена интерфейса (UIScene): Настройка перетаскивания

UIScene отвечает за драг-энд-дроп. Ее работа состоит из трех четких шагов.

1. **Создание интерактивного объекта:** Спрайт добавляется на сцену и сразу делается интерактивным. 2. **Назначение объекта как перетаскиваемого:** Важный вызов this.input.setDraggable(image) регистрирует этот объект в менеджере ввода сцены как доступный для перетаскивания. 3. **Подписка на события драга:** Сцена слушает три события: начало перетаскивания (dragstart), сам процесс (drag) и окончание (dragend). В обработчики передаются полезные данные, например, целевые координаты (dragX, dragY).

const image = this.add.sprite(200, 300, 'eye').setInteractive();
this.input.setDraggable(image);

this.input.on('dragstart', (pointer, gameObject) => {
    gameObject.setTint(0xff0000); // Подсветка красным при захвате
});

this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
    gameObject.x = dragX; // Непрерывное обновление позиции
    gameObject.y = dragY;
});

this.input.on('dragend', (pointer, gameObject) => {
    gameObject.clearTint(); // Снятие подсветки
});

Управление приоритетом ввода: `setGlobalTopOnly`

В коде GameScene есть закомментированная строка:

// this.input.setGlobalTopOnly(true);

Эта настройка — ключ к разрешению конфликтов между сценами. Если ее раскомментировать в GameScene, менеджер ввода этой сцены будет обрабатывать события **только от самого верхнего интерактивного объекта в стеке рендеринга**. Поскольку UIScene, вероятно, рендерится поверх (позже в массиве сцен), ее объект "глаз" будет считаться верхним. Следовательно, при клике на него GameScene проигнорирует событие, и коробка не изменит цвет. Это позволяет изолировать взаимодействия. Без этой настройки оба объекта получат событие pointerdown.

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

Пример демонстрирует фундаментальный принцип работы мульти-сцен и ввода в Phaser 3. Система по умолчанию рассылает события всем, но разработчик может тонко управлять этим поведением. Для экспериментов попробуйте

  1. Раскомментировать setGlobalTopOnly и понаблюдать за изоляцией сцен
  2. Добавить перетаскивание и для коробки в GameScene
  3. Создать третью, фоновую сцену и изменить порядок сцен в массиве конфига, чтобы увидеть, как он влияет на перехват событий