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

По умолчанию интерактивная зона (hit area) у игрового объекта в Phaser — это его текстура, ограниченная прямоугольником (bounding box). Это удобно, но иногда неточно: игрок может кликнуть по «пустому» месту спрайта, и событие сработает. А что, если нам нужна сложная форма, например, круг для круглого объекта? В этой статье разберем, как задать собственную геометрию для проверки кликов, используя метод `setInteractive` с кастомной функцией-обработчиком. Это особенно полезно для нестандартных спрайтов, изометрических игр или когда точность взаимодействия критична.

Версия 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('ball', 'assets/sprites/spikedball.png');
    }

    create ()
    {
        const shape = new Phaser.Geom.Circle(48, 48, 48);

        const ball1 = this.add.image(200, 300, 'ball').setInteractive(shape, this.handler);
        const ball2 = this.add.image(400, 300, 'ball').setInteractive(shape, this.handler);
        const ball3 = this.add.image(600, 300, 'ball').setInteractive(shape, this.handler);

        ball1.on('pointerdown', function ()
        {
            this.setTint(Math.random() * 16000000);
        });

        ball2.on('pointerdown', function ()
        {
            this.setTint(Math.random() * 16000000);
        });

        ball3.on('pointerdown', function ()
        {
            this.setTint(Math.random() * 16000000);
        });
    }

    handler (shape, x, y, gameObject)
    {
        if (shape.radius > 0 && x >= shape.left && x <= shape.right && y >= shape.top && y <= shape.bottom)
        {
            const dx = (shape.x - x) * (shape.x - x);
            const dy = (shape.y - y) * (shape.y - y);

            return (dx + dy) <= (shape.radius * shape.radius);
        }
        else
        {
            return false;
        }
    }
}

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

const game = new Phaser.Game(config);

Зачем нужна кастомная hit area?

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

Метод setInteractive у игрового объекта позволяет переопределить эту логику. Вы можете передать ему объект геометрии (например, Phaser.Geom.Circle) и собственную функцию для проверки попадания курсора в эту область.

const shape = new Phaser.Geom.Circle(48, 48, 48);
const ball = this.add.image(200, 300, 'ball').setInteractive(shape, this.handler);

В этом коде shape определяет геометрию, а handler — это наша функция, которая будет решать, был ли клик внутри этой фигуры.

Создание геометрии и настройка интерактивности

В примере мы создаем круглую геометрию, которая будет нашей целевой зоной. Ключевые параметры Phaser.Geom.Circle — это координаты центра и радиус.

const shape = new Phaser.Geom.Circle(48, 48, 48);

Здесь круг создан с центром в точке (48, 48) и радиусом 48 пикселей. Эти координаты относятся к локальной системе координат самого спрайта (от его верхнего левого угла). Поскольку текстура мяча имеет размер 96x96 пикселей, наш круг идеально вписывается в нее.

Далее мы создаем три спрайта мяча и для каждого вызываем setInteractive, передавая один и тот же объект shape и функцию this.handler.

const ball1 = this.add.image(200, 300, 'ball').setInteractive(shape, this.handler);

Важно: объект shape не клонируется, но для простых статических объектов, как в этом примере, это не проблема. Если форма должна быть уникальной для каждого объекта или меняться, создавайте новый экземпляр Phaser.Geom для каждого.

Пишем функцию-обработчик hit area

Сердце механизма — функция handler. Phaser будет вызывать ее каждый раз, когда курсор находится над объектом, чтобы проверить, попал ли он в нашу кастомную зону. Функция получает четыре аргумента.

handler (shape, x, y, gameObject) {
    // shape - переданный нами объект Phaser.Geom.Circle
    // x, y - координаты курсора в ЛОКАЛЬНОЙ системе объекта (относительно его вершины)
    // gameObject - ссылка на сам игровой объект (в нашем случае - ball)
}

Задача функции — вернуть true, если точка (x, y) находится внутри фигуры shape, и false — если снаружи.

В нашем примере реализована проверка для круга. Сначала идет быстрая проверка по ограничивающему прямоугольнику (bounding box) круга, чтобы отсечь заведомо неподходящие координаты.

if (shape.radius > 0 && x >= shape.left && x <= shape.right && y >= shape.top && y <= shape.bottom)

Если точка внутри этого прямоугольника, выполняется точная проверка на принадлежность кругу по формуле расстояния до центра.

const dx = (shape.x - x) * (shape.x - x);
const dy = (shape.y - y) * (shape.y - y);
return (dx + dy) <= (shape.radius * shape.radius);

Если условие не выполняется, функция возвращает false. Эта логика гарантирует, что клик сработает только внутри круглой области мяча, игнорируя прозрачные углы текстуры.

Добавляем реакцию на клик

После настройки кастомной зоны взаимодействия мы можем вешать на объект стандартные события ввода, например, pointerdown. Теперь они будут срабатывать только при клике внутри нашей фигуры.

ball1.on('pointerdown', function () {
    this.setTint(Math.random() * 16000000);
});

В этом обработчике this ссылается на игровой объект (ball1). Метод setTint применяет случайный цветовой оттенок к спрайту, наглядно демонстрируя успешное взаимодействие.

Вы можете использовать любые другие события (pointerover, pointerout, pointerup) и любую логику внутри — изменение текстуры, запуск анимации, нанесение урона. Механика проверки попадания в зону остается за нашей функцией handler.

Конфигурация игры и запуск

Сцена Example регистрируется в конфигурации игры, как это обычно делается в Phaser 3.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example // Указываем нашу сцену с кастомными hit areas
};

const game = new Phaser.Game(config);

Эта конфигурация создает игровой холст размером 800x600 пикселей. Все три мяча с их круглыми областями клика будут отрисованы и полностью функциональны.

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

Использование кастомной функции для определения hit area дает полный контроль над зоной взаимодействия игрового объекта. Вы не ограничены кругом — можно реализовать проверку для полигона, эллипса или даже составной фигуры. **Идеи для экспериментов:** 1. Попробуйте использовать Phaser.Geom.Polygon для спрайта сложной формы (например, звезды). 2. Сделайте «дырку» в центре круга, чтобы клик срабатывал только в кольце. 3. Динамически меняйте размер или положение фигуры shape в функции handler в зависимости от состояния объекта (например, при получении урона зона клика уменьшается). 4. Оптимизируйте производительность: для статических объектов кэшируйте результат проверки или используйте более простые геометрические аппроксимации.