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

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

Версия 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.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
    }

    create ()
    {
        //  Create a little 32x32 texture to use to show where the mouse is
        const graphics = this.make.graphics({ x: 0, y: 0, add: false, fillStyle: { color: 0xff00ff, alpha: 1 } });

        graphics.fillRect(0, 0, 32, 32);

        graphics.generateTexture('block', 32, 32);

        const highlighted = this.add.image(16, 16, 'block');

        const hitArea = new Phaser.Geom.Rectangle(0, 0, 32, 32);
        const hitAreaCallback = Phaser.Geom.Rectangle.Contains;

        //  Create 400 sprites aligned in a grid
        const group = this.make.group({
            classType: Phaser.GameObjects.Image,
            key: 'bobs',
            frame: Phaser.Utils.Array.NumberArray(0, 399),
            randomFrame: true,
            hitArea: hitArea,
            hitAreaCallback: hitAreaCallback,
            gridAlign: {
                width: 25,
                height: 25,
                cellWidth: 32,
                cellHeight: 32
            }
        });

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

            highlighted.setPosition(gameObject.x, gameObject.y);

        });
    }
}

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

const game = new Phaser.Game(config);

Подготовка текстур и настройка сцены

В методе preload() загружается спрайтшит, содержащий 400 кадров. Это основа для создания нашей сетки объектов.

this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });

В create() первым делом создается небольшая текстурка размером 32x32 пикселя. Она будет служить визуальным маркером, показывающим, над каким спрайтом сейчас находится курсор. Для этого используется объект Graphics.

const graphics = this.make.graphics({ x: 0, y: 0, add: false, fillStyle: { color: 0xff00ff, alpha: 1 } });
graphics.fillRect(0, 0, 32, 32);
graphics.generateTexture('block', 32, 32);
const highlighted = this.add.image(16, 16, 'block');

Ключевой параметр add: false указывает, что объект Graphics не нужно автоматически добавлять на дисплей списка сцены, так как он нужен только для генерации текстуры.

Секрет оптимизации: HitArea и HitAreaCallback

Вместо того чтобы вручную настраивать физические тела или обрабатывать пересечения для каждого спрайта, Phaser позволяет определить единую логику проверки попадания (hit-test) для целой группы объектов. Для этого используются два свойства: hitArea и hitAreaCallback.

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

const hitArea = new Phaser.Geom.Rectangle(0, 0, 32, 32);

Затем задается функция-коллбек, которая будет проверять, находится ли точка (координаты курсора) внутри этой области. В данном случае используется встроенная статическая функция Phaser.Geom.Rectangle.Contains.

const hitAreaCallback = Phaser.Geom.Rectangle.Contains;

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

Создание интерактивной сетки объектов

Создание 400 спрайтов выполняется одной мощной операцией через this.make.group. Конфигурационный объект группы содержит все необходимые настройки.

const group = this.make.group({
    classType: Phaser.GameObjects.Image,
    key: 'bobs',
    frame: Phaser.Utils.Array.NumberArray(0, 399),
    randomFrame: true,
    hitArea: hitArea,
    hitAreaCallback: hitAreaCallback,
    gridAlign: {
        width: 25,
        height: 25,
        cellWidth: 32,
        cellHeight: 32
    }
});

Разберем ключевые параметры: * classType: Указывает, что каждый элемент группы будет Image. * frame: Phaser.Utils.Array.NumberArray(0, 399) создает массив чисел от 0 до 399, задавая уникальный кадр спрайтшита для каждого объекта. * randomFrame: true перемешивает этот массив, чтобы кадры распределялись случайно. * hitArea и hitAreaCallback: Применяют ранее созданные область и функцию проверки ко всем спрайтам. * gridAlign: Автоматически выравнивает 400 спрайтов в сетку 25x16 (поскольку 25*16=400) с шагом в 32 пикселя.

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

Благодаря заданным hitArea и hitAreaCallback, все спрайты в группе автоматически становятся интерактивными и начинают испускать события ввода, такие как gameobjectover (наведение курсора).

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

this.input.on('gameobjectover', (pointer, gameObject) => {
    highlighted.setPosition(gameObject.x, gameObject.y);
});

Phaser сам определяет, какой именно спрайт в группе стал целью события, и передает его в коллбек. Нам не нужно писать логику для поиска этого объекта.

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

Использование hitAreaCallback — это мощный паттерн для оптимизации взаимодействия с большими наборами статичных объектов одинаковой формы. Он переносит логику проверки из уровня отдельных объектов на уровень группы, делая код чище, а производительность — выше. Для экспериментов попробуйте заменить Phaser.Geom.Rectangle.Contains на собственную функцию, например, для круглой области или области со смещением. Также можно обрабатывать другие события, такие как gameobjectdown (клик) или gameobjectout (уход курсора), чтобы создавать сложные интерфейсы, например, для карт или инвентаря.