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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    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 sprite1 = this.add.sprite(400, 300, 'eye').setInteractive();
        const sprite2 = this.add.sprite(450, 350, 'eye').setInteractive();

        sprite1.name = 'bob';
        sprite2.name = 'ben';

        this.input.topOnly = true;

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

            gameObject.setTint(0x00ff00);

        });

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

            if (gameObject.input.isDown)
            {
                gameObject.setTint(0xff0000);
            }
            else
            {
                gameObject.clearTint();
            }

        });

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

            gameObject.setTint(0xff0000);

        });

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

            if (gameObject.input.isOver)
            {
                gameObject.setTint(0x00ff00);
            }
            else
            {
                gameObject.clearTint();
            }

        });
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Суть свойства topOnly

Свойство this.input.topOnly — это флаг, управляющий поведением системы ввода Phaser. По умолчанию оно имеет значение false. В этом режиме, если курсор мыши находится над несколькими интерактивными объектами одновременно (они перекрывают друг друга), событие (например, gameobjectover) будет отправлено каждому из этих объектов.

Когда topOnly установлено в true, система ввода меняет свое поведение. Она определяет, какой объект в стеке отрисовки является самым верхним (имеет наибольшую глубину или был добавлен последним), и отправляет события только ему. Это имитирует стандартное поведение веб-страниц и большинства десктопных интерфейсов, где взаимодействие происходит только с видимым элементом поверх остальных.

this.input.topOnly = true;

Настройка сцены и интерактивных спрайтов

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

Каждому спрайту присваивается уникальное имя через свойство .name. Это не влияет на логику topOnly, но полезно для отладки и идентификации объектов в более сложных проектах. Спрайты расположены так, что второй (sprite2) частично перекрывает первый (sprite1).

const sprite1 = this.add.sprite(400, 300, 'eye').setInteractive();
const sprite2 = this.add.sprite(450, 350, 'eye').setInteractive();

sprite1.name = 'bob';
sprite2.name = 'ben';

Обработка событий наведения (over/out)

Событие gameobjectover срабатывает, когда курсор входит в область интерактивного объекта. В нашем примере при наведении спрайт окрашивается в зеленый цвет (0x00ff00). Благодаря topOnly = true, зеленым станет только верхний спрайт под курсором, даже если он частично нависает над нижним.

Событие gameobjectout происходит, когда курсор покидает объект. Логика здесь сложнее: нужно проверить, была ли зажата кнопка мыши на этом объекте (свойство gameObject.input.isDown). Если да, спрайт остается окрашенным в красный (состояние "нажатия"), если нет — окраска сбрасывается.

this.input.on('gameobjectover', (pointer, gameObject) => {
    gameObject.setTint(0x00ff00);
});

this.input.on('gameobjectout', (pointer, gameObject) => {
    if (gameObject.input.isDown) {
        gameObject.setTint(0xff0000);
    } else {
        gameObject.clearTint();
    }
});

Обработка событий клика (down/up)

Событие gameobjectdown соответствует нажатию кнопки мыши над объектом. В ответ спрайт окрашивается в красный цвет (0xff0000).

Событие gameobjectup — это отпускание кнопки мыши. Здесь важно проверить, находится ли курсор все еще над этим объектом (gameObject.input.isOver). Если да (то есть это был клик, а не перенос с зажатой кнопкой), спрайт меняет цвет на зеленый (состояние "наведения"). Если курсор уже ушел с объекта, окраска сбрасывается.

this.input.on('gameobjectdown', (pointer, gameObject) => {
    gameObject.setTint(0xff0000);
});

this.input.on('gameobjectup', (pointer, gameObject) => {
    if (gameObject.input.isOver) {
        gameObject.setTint(0x00ff00);
    } else {
        gameObject.clearTint();
    }
});

Почему важен контекст isOver и isDown

Ключевой момент в логике примера — использование свойств gameObject.input.isOver и gameObject.input.isDown. Эти флаги хранят текущее состояние взаимодействия для конкретного игрового объекта.

* isOver: true, если курсор в данный момент находится над этим объектом. * isDown: true, если на этом объекте была нажата кнопка мыши и еще не отпущена.

В режиме topOnly = true эти свойства корректно обновляются только для верхнего объекта. Например, если зажать кнопку на верхнем спрайте и, не отпуская, переместить курсор на нижний, у нижнего спрайта isDown останется false, так как событие gameobjectdown ему не отправлялось. Это предотвращает "липкое" поведение и путаницу в состояниях.

Именно эти свойства позволяют в обработчиках gameobjectout и gameobjectup принять правильное решение о смене цвета, учитывая не только факт события, но и текущий контекст взаимодействия.

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

Свойство this.input.topOnly — это мощный и простой инструмент для управления приоритетом ввода в Phaser 3. Его использование делает взаимодействие с перекрывающимися элементами интуитивно понятным для игрока, так как соответствует общепринятым паттернам UI. **Идеи для экспериментов:** 1. Установите topOnly = false и понаблюдайте, как оба спрайта одновременно реагируют на наведение и клик. 2. Создайте стек из 3-4 интерактивных объектов с разной прозрачностью и отслеживайте, какой именно из них получает события. 3. Скомбинируйте topOnly с перетаскиванием (setInteractive({ draggable: true })). Попробуйте перетащить нижний спрайт из-под верхнего — это не сработает, пока topOnly активно, что логично для большинства интерфейсов.