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

Создание сложных интерактивных элементов, таких как кнопки с иконками или составные персонажи, часто требует объединения нескольких игровых объектов. Phaser предоставляет для этого мощный инструмент — `Container`. В этой статье мы разберем, как правильно настраивать интерактивность для контейнера и его дочерних элементов, чтобы избежать конфликтов и создать отзывчивый интерфейс. Вы узнаете, как назначать хит-боксы разной формы и грамотно распределять обработчики событий.

Версия 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('buttonBG', 'assets/sprites/button-bg.png');
        this.load.image('buttonText', 'assets/sprites/button-text.png');
    }

    create ()
    {
        const bg = this.add.image(0, 0, 'buttonBG');
        const text = this.add.image(0, 0, 'buttonText');

        const bg2 = this.add.image(0, 80, 'buttonBG');
        const text2 = this.add.image(0, 80, 'buttonText');

        const container = this.add.container(400, 200, [ bg, text, bg2, text2 ]);

        container.setInteractive(new Phaser.Geom.Circle(0, 0, 60), Phaser.Geom.Circle.Contains);

        bg2.setInteractive();

        container.on('pointerover', () =>
        {

            bg.setTint(0x44ff44);

        });

        container.on('pointerout', () =>
        {

            bg.clearTint();

        });

        bg2.on('pointerover', function ()
        {

            this.setTint(0xff44ff);

        });

        bg2.on('pointerout', function ()
        {

            this.clearTint();

        });

        //  Just to display the hit area, not actually needed to work
        const graphics = this.add.graphics();

        graphics.lineStyle(2, 0x00ffff, 1);

        graphics.strokeCircle(container.x, container.y, container.input.hitArea.radius);
    }
}

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

const game = new Phaser.Game(config);

Что такое контейнер и зачем он нужен

Контейнер (Phaser.GameObjects.Container) — это специальный игровой объект, который позволяет группировать другие объекты (изображения, текст, спрайты) и управлять ими как единым целым. Вы можете перемещать, масштабировать или вращать весь контейнер, и все его дети будут трансформироваться вместе с ним.

Это особенно полезно для создания сложных UI-элементов. В нашем примере кнопка состоит из двух частей: фона (buttonBG) и текста (buttonText). Оба изображения загружаются отдельно, но затем помещаются в контейнер.

const bg = this.add.image(0, 0, 'buttonBG');
const text = this.add.image(0, 0, 'buttonText');
const container = this.add.container(400, 200, [ bg, text ]);

Ключевой момент: координаты дочерних объектов (0, 0) задаются относительно позиции самого контейнера (400, 200). Это локальная система координат контейнера.

Настройка интерактивности контейнера

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

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

container.setInteractive(new Phaser.Geom.Circle(0, 0, 60), Phaser.Geom.Circle.Contains);

Первый аргумент — объект геометрии (Phaser.Geom.Circle). Второй аргумент — функция (Phaser.Geom.Circle.Contains), которая будет проверять, попал ли указатель в эту область. После этого на контейнер можно вешать стандартные слушатели событий pointerover и pointerout.

container.on('pointerover', () => {
    bg.setTint(0x44ff44);
});

Конфликт интерактивности: контейнер против ребенка

Особенность, которую демонстрирует пример — возможность независимой интерактивности у дочернего объекта. В контейнер добавлена вторая пара изображений (фон bg2 и текст text2), смещенная на 80 пикселей вниз.

Для второго фона (bg2) также вызывается setInteractive. Теперь у нас есть две активные сущности: сам контейнер (с круглым хит-боксом) и один из его детей.

bg2.setInteractive();
bg2.on('pointerover', function () {
    this.setTint(0xff44ff);
});

Когда указатель наводится на bg2, срабатывает его собственный обработчик (pointerover), окрашивающий его в фиолетовый. Событие не "всплывает" к контейнеру в этот момент, что позволяет создавать разные реакции для разных частей составного объекта. Это мощный механизм для создания сложной логики взаимодействия.

Визуализация хит-бокса для отладки

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

const graphics = this.add.graphics();
graphics.lineStyle(2, 0x00ffff, 1);
graphics.strokeCircle(container.x, container.y, container.input.hitArea.radius);

Здесь создается объект graphics, настраивается стиль линии и рисуется контур круга. Координаты круга берутся из позиции контейнера, а радиус — из созданного ранее объекта Phaser.Geom.Circle, который хранится в container.input.hitArea. Этот подход помогает точно настроить область клика во время разработки.

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

Контейнеры в Phaser — это фундаментальный инструмент для структурирования сложных игровых объектов и UI. Главное — помнить о двух уровнях интерактивности: общий для всего контейнера и точечный для отдельных его частей. Для экспериментов попробуйте: изменить форму хит-бокса на прямоугольник (Phaser.Geom.Rectangle), сделать интерактивными несколько дочерних объектов одновременно или заставить событие от ребенка "всплывать" к контейнеру, используя emit или флаги события.