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

При наведении курсора на перекрывающиеся игровые объекты Phaser может генерировать события для всех объектов под указателем или только для верхнего. Это поведение контролируется методом `setTopOnly()`. Понимание этой механики критично для создания сложных интерфейсов, инвентарей или интерактивных сцен, где объекты наслаиваются друг на друга. В статье разберем, как работает приоритизация событий и когда отключать `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 ()
    {
        for (let i = 0; i < 14; i++)
        {
            this.add.sprite(100 + i * 30, 100 + i * 30, 'eye').setInteractive();
        }

        //  If you disable topOnly it will fire events for all objects the pointer is over
        //  regardless of their place on the display list
        this.input.setTopOnly(true);

        //  Events

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

            gameObject.setTint(0xff0000);

        });

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

            gameObject.clearTint();

        });

        const text = this.add.text(10, 10, 'Input.topOnly: true', { font: '16px Courier', fill: '#00ff00' });

        this.input.on('pointerdown', function (pointer)
        {
        
            if (this.input.topOnly)
            {
                this.input.setTopOnly(false);
                text.setText('Input.topOnly: false');
            }
            else
            {
                this.input.setTopOnly(true);
                text.setText('Input.topOnly: true');
            }

        
        }, this);
    }
}

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

const game = new Phaser.Game(config);

Суть параметра TopOnly

По умолчанию Phaser использует режим topOnly: true. Это означает, что события ввода, такие как 'gameobjectover' (наведение) и 'gameobjectout' (сведение), будут срабатывать только для самого верхнего интерактивного объекта в стеке отображения под курсором. Нижележащие объекты игнорируются, даже если указатель находится и над ними.

Если установить topOnly: false, система будет проверять все объекты под указателем и генерировать события для каждого из них. Это полезно, когда нужно одновременно подсветить несколько перекрывающихся элементов.

Настройка сцены и создание объектов

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

this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
for (let i = 0; i < 14; i++)
{
    this.add.sprite(100 + i * 30, 100 + i * 30, 'eye').setInteractive();
}

Изначально режим устанавливается в true.

this.input.setTopOnly(true);

Обработка событий наведения и сведения

Регистрируются обработчики для событий 'gameobjectover' и 'gameobjectout'. Когда курсор попадает на объект (over), ему задается красный оттенок (setTint). Когда курсор уходит с объекта (out), оттенок снимается (clearTint).

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

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

В режиме topOnly: true эти события будут возникать только для верхнего спрайта в точке пересечения.

Переключение режима в runtime

В примере добавлено переключение режима по клику ('pointerdown'). При каждом клике проверяется текущее состояние this.input.topOnly и оно инвертируется с помощью setTopOnly(). Текстовый объект обновляется, чтобы отображать текущий режим.

this.input.on('pointerdown', function (pointer)
{
    if (this.input.topOnly)
    {
        this.input.setTopOnly(false);
        text.setText('Input.topOnly: false');
    }
    else
    {
        this.input.setTopOnly(true);
        text.setText('Input.topOnly: true');
    }
}, this);

Это позволяет наглядно сравнить поведение системы при разных настройках прямо во время работы примера.

Практические отличия в поведении

1. **При topOnly: true**: Наведите курсор на область, где спрайты перекрываются. Подсветится (станет красным) только один, самый верхний спрайт. При движении курсора события out и over будут срабатывать быстро, так как система отслеживает только верхний объект.

2. **При topOnly: false**: Наведите курсор на ту же область. Красными станут все спрайты, находящиеся под курсором. При движении мыши события будут генерироваться для каждого объекта в стеке, что может привести к более сложным цепочкам вызовов. Это хорошо видно, если медленно вести курсор по цепочке спрайтов.

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

Метод setTopOnly — это мощный инструмент для управления всплытием событий в сложных интерфейсах. Используйте режим true для обычных кнопок и одиночных взаимодействий, где важен только верхний элемент. Режим false пригодится для составных объектов, кастомных курсоров, влияющих на несколько сущностей сразу, или для отладки наложения коллайдеров. Поэкспериментируйте: создайте инвентарь с перекрывающимися ячейками или меню с выпадающими списками, чтобы почувствовать разницу.