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

В сложных играх часто используется несколько сцен, работающих одновременно: например, HUD поверх игрового мира или меню в углу экрана. Возникает вопрос: если в разных сценах есть интерактивные объекты, как Phaser определяет, по какому из них кликнули? Эта статья разбирает пример с тремя активными сценами и объясняет механизм обработки событий ввода. Понимание этого принципа поможет избежать багов с "проглатыванием" кликов и правильно организовывать UI в ваших проектах.

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

Живой запуск

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

Исходный код


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

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

    create ()
    {
        const bg = this.add.image(400, 300, 'bg').setInteractive();

        bg.on('pointerdown', () =>
        {
            console.log('Scene A');
        });
    }
}

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('up', 'assets/ui/up-bubble.png');
    }

    create ()
    {
        const button = this.add.image(400, 300, 'up').setInteractive();

        button.on('pointerdown', () =>
        {
            console.log('Scene B');
        });
    }
}

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

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

    create ()
    {
        const button = this.add.image(500, 300, 'down').setInteractive();

        button.on('pointerdown', () =>
        {
            console.log('Scene C');
        });
    }
}

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. Ключевой момент — все они объявлены как active: true в конструкторе.

super({ key: 'sceneA', active: true });

Это означает, что сцена будет немедленно запущена после загрузки и будет обрабатывать ввод и обновление. Все три сцены работают параллельно, и каждая добавляет на экран свой графический элемент (изображение), делая его интерактивным с помощью метода .setInteractive().

Создание интерактивных объектов

В методе create() каждой сцены создается изображение и назначается обработчик события pointerdown. Например, в SceneB:

const button = this.add.image(400, 300, 'up').setInteractive();
button.on('pointerdown', () => {
    console.log('Scene B');
});

Метод .setInteractive() без параметров назначает на изображение хитбокс, совпадающий с его размером. Теперь объект будет генерировать события ввода. Важно: координаты объектов частично перекрываются (SceneA и SceneB имеют одинаковые координаты 400, 300).

Принцип стека сцен и порядок обработки

Phaser хранит активные сцены в стеке (списке). Когда происходит событие ввода (например, клик мыши), система начинает проверку с **самой верхней сцене в стеке**. В данном примере порядок добавления сцен в массив scene: [ SceneA, SceneB, SceneC ] определяет их начальный порядок в стеке.

Алгоритм такой: 1. Phaser берет самую верхнюю сцену (условно, последнюю добавленную или активированную). 2. Проверяет, был ли клик по интерактивному объекту внутри этой сцены. 3. Если да — срабатывает обработчик этой сцены, а проверка для сцен ниже **прекращается**. Событие считается обработанным. 4. Если нет — Phaser переходит к проверке следующей сцены в стеке.

Таким образом, если объекты перекрываются, "выиграет" тот, что принадлежит сцене, находящейся выше в стеке.

Почему важен порядок в массиве сцен

В конфигурации игры сцены передаются в массив. Порядок в этом массиве влияет на начальный порядок в стеке. В нашем примере:

scene: [ SceneA, SceneB, SceneC ]

Предположительно, SceneC (последняя в массиве) окажется самой верхней. Если кликнуть в точке (500, 300) (где находится только объект из SceneC), в консоль выведется "Scene C". Если же кликнуть в точке (400, 300), где объекты из SceneA и SceneB перекрываются, сработает обработчик сцены, которая выше в стеке (скорее всего, SceneB или SceneC, в зависимости от внутреннего порядка активации).

Порядок можно динамически менять методами вроде scene.bringToTop(), что полезно для всплывающих окон.

Практический вывод: как избежать конфликтов

1. **Планируйте слои UI:** Делайте фоновые (игровые) сцены нижними в стеке, а элементы интерфейса (кнопки, меню) — верхними. 2. **Используйте прозрачные области:** Если нужно, чтобы клик "прошел" сквозь верхний слой к нижнему (например, сквозь полупрозрачную панель), можно не делать объект интерактивным или обработать событие иначе. 3. **Проверяйте логи:** Всегда проверяйте в консоли, обработчик какой именно сцены срабатывает при клике в спорной зоне. 4. **Управляйте активностью:** Сценам, которые не должны реагировать на ввод, можно выключать обработку ввода через this.input.enabled = false.

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

Phaser обрабатывает события указателя (клики, касания) по интерактивным объектам, начиная с самой верхней сцены в стеке. Это позволяет создавать сложные интерфейсы с перекрывающимися элементами. Для экспериментов попробуйте

  1. изменить порядок сцен в массиве конфига и посмотреть, какой объект теперь "перехватывает" клик
  2. динамически менять порядок сцен с помощью scene.bringToTop() в runtime
  3. добавить четвертую сцену с полностью прозрачным, но интерактивным объектом, который блокирует клики по всем остальным