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

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

Версия 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/wizball.png');
        this.load.image('chick', 'assets/sprites/budbrain_chick.png');
        this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
        this.load.image('ship', 'assets/sprites/thrust_ship2.png');
        this.load.image('car', 'assets/pics/supercars-parsec.png');
    }

    create ()
    {
        const sprite1 = this.add.sprite(200, 550, 'car').setOrigin(0);

        sprite1.setInteractive(new Phaser.Geom.Polygon([ 0, 143, 0, 92, 110, 40, 244, 4, 330, 0, 458, 12, 574, 18, 600, 79, 594, 153, 332, 152, 107, 157 ]), Phaser.Geom.Polygon.Contains);

        this.input.setDraggable(sprite1);

        const sprite2 = this.add.sprite(150, 150, 'ball').setScale(2);

        sprite2.setInteractive(new Phaser.Geom.Circle(45, 46, 45), Phaser.Geom.Circle.Contains);

        const sprite3 = this.add.sprite(600, 200, 'chick').setScale(2);

        sprite3.setInteractive(new Phaser.Geom.Ellipse(33, 65, 66, 133), Phaser.Geom.Ellipse.Contains);

        const sprite4 = this.add.sprite(350, 300, 'eye');

        sprite4.setInteractive(new Phaser.Geom.Rectangle(0, 0, 128, 128), Phaser.Geom.Rectangle.Contains);

        const sprite5 = this.add.sprite(850, 350, 'ship').setScale(8);

        sprite5.setInteractive(new Phaser.Geom.Triangle.BuildEquilateral(sprite5.displayOriginX, 0, 32), Phaser.Geom.Triangle.Contains);

        //  Specify a different debug outline color
        this.input.enableDebug(sprite1, 0xff00ff);
        this.input.enableDebug(sprite2, 0xffff00);
        this.input.enableDebug(sprite3);
        this.input.enableDebug(sprite4);
        this.input.enableDebug(sprite5);

        this.tweens.add({
            targets: sprite3,
            angle: 360,
            repeat: -1,
            duration: 6000
        });

        this.tweens.add({
            targets: sprite4,
            scale: 2,
            yoyo: true,
            repeat: -1,
            duration: 4000
        });

        //  Input Event listeners

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

            gameObject.setTint(0x7878ff);

        });

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

            gameObject.clearTint();

        });

        sprite1.on('drag', (pointer, dragX, dragY) =>
        {

            sprite1.x = dragX;
            sprite1.y = dragY;

        });

        sprite2.on('pointerdown', () => {

            sprite2.visible = !sprite2.visible;

        });
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    pixelArt: true,
    backgroundColor: '#00a99d',
    scene: Example
};

const game = new Phaser.Game(config);

Проблема прямоугольного хитбокса

Когда вы делаете спрайт интерактивным с помощью setInteractive() без параметров, Phaser автоматически создает для него хитбокс в виде прямоугольника (bounding box), который охватывает все пиксели изображения. Для круглого мяча или космического корабля с прозрачными областями это приводит к нежелательному эффекту: игрок может кликнуть «мимо» видимой части спрайта, но все равно активировать событие.

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

Задаем произвольную геометрию хитбокса

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

В примере создается пять спрайтов с разными формами хитбоксов:

// Полигональная форма для машины
sprite1.setInteractive(new Phaser.Geom.Polygon([0, 143, 0, 92, 110, 40, 244, 4, 330, 0, 458, 12, 574, 18, 600, 79, 594, 153, 332, 152, 107, 157]), Phaser.Geom.Polygon.Contains);

// Круг для мяча (задан центр и радиус)
sprite2.setInteractive(new Phaser.Geom.Circle(45, 46, 45), Phaser.Geom.Circle.Contains);

// Эллипс для цыпленка (x, y, width, height)
sprite3.setInteractive(new Phaser.Geom.Ellipse(33, 65, 66, 133), Phaser.Geom.Ellipse.Contains);

// Прямоугольник для глаза (x, y, width, height)
sprite4.setInteractive(new Phaser.Geom.Rectangle(0, 0, 128, 128), Phaser.Geom.Rectangle.Contains);

// Равносторонний треугольник для корабля (x, y, size)
sprite5.setInteractive(new Phaser.Geom.Triangle.BuildEquilateral(sprite5.displayOriginX, 0, 32), Phaser.Geom.Triangle.Contains);

Обратите внимание: координаты фигуры задаются относительно **начала координат спрайта** (origin). Если origin не равен (0,0), это нужно учитывать.

Визуализация для отладки: enableDebug

Ключевой метод для отладки — this.input.enableDebug(). Он рисует поверх игрового объекта контур его активной хит-области.

// Включение отладки с кастомным цветом (шестнадцатеричный код)
this.input.enableDebug(sprite1, 0xff00ff); // Пурпурный
this.input.enableDebug(sprite2, 0xffff00); // Желтый

// Включение отладки с цветом по умолчанию (зеленый)
this.input.enableDebug(sprite3);

Этот контур виден только во время разработки и не мешает игровому процессу. Он позволяет одним взглядом проверить, правильно ли вы задали полигон для сложного спрайта или круг для шарообразного объекта.

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

После настройки хитбоксов можно назначать обработчики событий. В примере показано несколько типичных сценариев.

Глобальные события gameobjectover и gameobjectout срабатывают при наведении и уводе курсора с любого интерактивного объекта. В примере они применяют и снимают tint (оттенок).

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

Для конкретного объекта можно назначить уникальные события. Машина (sprite1) стала перетаскиваемой, а по клику на мяч (sprite2) он скрывается или появляется.

// Делаем спрайт перетаскиваемым
this.input.setDraggable(sprite1);

// Обработка перетаскивания: обновляем координаты спрайта
sprite1.on('drag', (pointer, dragX, dragY) => {
    sprite1.x = dragX;
    sprite1.y = dragY;
});

// По клику переключаем видимость мяча
sprite2.on('pointerdown', () => {
    sprite2.visible = !sprite2.visible;
});

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

Использование setInteractive() с геометрическими фигурами и enableDebug() для визуализации — мощный дуэт для отладки взаимодействий в Phaser. Это избавляет от догадок и делает процесс точным. Для экспериментов попробуйте: изменить координаты полигона у машины в реальном времени, использовать Phaser.Geom.Triangle.BuildRight для создания корабля другой формы или привязать отладочный контур к камере, чтобы он оставался на экране при ее скролле.