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

При разработке игр часто возникает задача быстрого поиска объектов в определённой области экрана. Например, для проверки столкновений курсора с объектами, поиска врагов в зоне видимости или определения объектов для атаки. Перебор всех игровых спрайтов при каждом движении мыши — операция ресурсоёмкая, особенно если объектов сотни. На помощь приходит структура данных R-Tree, реализованная в Phaser как `Phaser.Structs.RTree`. Эта статья покажет, как использовать R-Tree для эффективного поиска объектов в прямоугольной области, что значительно повысит производительность ваших игр при работе с большим количеством объектов на сцене.

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

    create ()
    {
        //  Create an RTree

        const tree = new Phaser.Structs.RTree();

        for (let i = 0; i < 512; i++)
        {
            const ship = this.add.image(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 590), 'ship');

            const bounds = ship.getBounds();

            //  Insert our entry into the RTree:
            tree.insert({ left: bounds.left, right: bounds.right, top: bounds.top, bottom: bounds.bottom, sprite: ship });
        }

        const debug = this.add.graphics();

        debug.lineStyle(1, 0x00ff00);

        let results = [];

        this.input.on('pointermove', pointer =>
        {

            //  First clear the previous results
            results.forEach(entry =>
            {

                entry.sprite.setTint(0xffffff);

            });

            debug.clear();

            //  Update the search area

            const bbox = {
                minX: pointer.x - 100,
                minY: pointer.y - 100,
                maxX: pointer.x + 100,
                maxY: pointer.y + 100
            };

            //  Search the RTree

            results = tree.search(bbox);

            //  Set Tint on intersecting Sprites

            results.forEach(entry =>
            {

                entry.sprite.setTint(0xff0000);

            });

            //  Draw debug

            debug.strokeRect(bbox.minX, bbox.minY, 200, 200);

        }, this);
    }
}

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

const game = new Phaser.Game(config);

Что такое R-Tree и зачем он нужен?

R-Tree — это древовидная структура данных, индексирующая пространственные объекты по их ограничивающим прямоугольникам (bounding boxes). Вместо последовательного перебора всех объектов для нахождения тех, что пересекаются с заданной областью, RTree выполняет поиск за логарифмическое время.

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

Пример из статьи как раз демонстрирует эту задачу: он находит все спрайты кораблей в пределах квадрата 200x200 пикселей, следующего за курсором.

Создание RTree и наполнение данными

Первым делом создаётся экземпляр RTree. Затем для каждого игрового объекта вычисляется его ограничивающий прямоугольник и записывается в дерево вместе со ссылкой на сам объект.

const tree = new Phaser.Structs.RTree();

В цикле создаются спрайты и для каждого из них получаются границы. Ключевой момент — вставка данных в RTree. Данные должны представлять собой объект с полями, описывающими прямоугольник (left, right, top, bottom), и любыми другими пользовательскими данными (в нашем случае — ссылкой на спрайт).

const ship = this.add.image(Phaser.Math.Between(0, 800), Phaser.Math.Between(0, 590), 'ship');
const bounds = ship.getBounds();
tree.insert({ left: bounds.left, right: bounds.right, top: bounds.top, bottom: bounds.bottom, sprite: ship });

Метод getBounds() возвращает прямоугольник Phaser.Geom.Rectangle, описывающий габариты спрайта в мировых координатах.

Организация поиска по области

Поиск в RTree запускается в обработчике события движения указателя pointermove. Для этого необходимо определить область поиска — прямоугольник (bbox — bounding box).

const bbox = {
    minX: pointer.x - 100,
    minY: pointer.y - 100,
    maxX: pointer.x + 100,
    maxY: pointer.y + 100
};

Обратите внимание, что RTree в Phaser ожидает на вход объект с полями minX, minY, maxX, maxY, а не left, right и т.д. Это важно для корректной работы.

Затем выполняется собственно поиск:

results = tree.search(bbox);

Метод search() возвращает массив всех записей (тех самых объектов, которые мы вставляли через insert), чьи прямоугольники пересекаются с заданным bbox.

Визуализация и обработка результатов

После получения результатов их нужно обработать. В примере это делается в два этапа: 1. Сброс визуального выделения у всех объектов из предыдущего результата поиска. 2. Применение выделения (установка красного оттенка) к объектам из нового результата.

// Сброс предыдущего выделения
results.forEach(entry => {
    entry.sprite.setTint(0xffffff);
});

// Применение нового выделения к найденным спрайтам
results.forEach(entry => {
    entry.sprite.setTint(0xff0000);
});

Для наглядности область поиска также отрисовывается на экране с помощью графического объекта debug.

debug.strokeRect(bbox.minX, bbox.minY, 200, 200);

Важно не забывать очищать графику (debug.clear()) и сбрасывать выделение перед каждым новым поиском, чтобы визуализация соответствовала текущему кадру.

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

Использование Phaser.Structs.RTree — это мощный приём для оптимизации пространственных запросов в играх. Вы можете адаптировать этот подход не только для взаимодействия с курсором, но и для расчёта зоны видимости NPC, определения объектов в радиусе действия заклинания или оптимизации широкофазного обнаружения столкновений. Попробуйте поэкспериментировать: измените размер области поиска в зависимости от скорости движения курсора, реализуйте выделение объектов по клику с помощью tree.search(), или создадите динамическое дерево, которое будет обновляться (tree.clear() + перевставка) для движущихся объектов.