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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    group;
    player;
    cursors;
    controls;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('chunk', 'assets/sprites/space-baddie.png');
        this.load.image('crate', 'assets/sprites/crate.png');
    }

    create ()
    {
        this.physics.world.setBounds(0, 0, 800 * 1, 600 * 1);

        const bounds = new Phaser.Geom.Rectangle(30, 30, 300, 540);

        this.group = this.physics.add.group({ immovable: true });

        for (let i = 0; i < 50; i++)
        {
            const pos = bounds.getRandomPoint();

            this.group.create(pos.x, pos.y, 'chunk');
        }

        this.cursors = this.input.keyboard.createCursorKeys();

        this.player = this.physics.add.image(600, 300, 'crate');

        this.physics.add.collider(this.player, this.group);
    }

    update ()
    {
        this.player.setVelocity(0);

        if (this.cursors.left.isDown)
        {
            this.player.setVelocityX(-500);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.setVelocityX(500);
        }

        if (this.cursors.up.isDown)
        {
            this.player.setVelocityY(-500);
        }
        else if (this.cursors.down.isDown)
        {
            this.player.setVelocityY(500);
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 0 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

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

Широкофазное обнаружение столкновений (broad-phase collision detection) — это этап, на котором система определяет, какие пары объектов находятся достаточно близко, чтобы проверять их на реальное пересечение (узкофазная проверка).

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

В Arcade Physics R-Tree используется по умолчанию для статических и неподвижных тел, что мы и видим в примере с группой (immovable: true).

Настройка сцены и создание статической группы

В методе create() происходит инициализация физического мира и создание группы статических препятствий.

this.physics.world.setBounds(0, 0, 800 * 1, 600 * 1);
const bounds = new Phaser.Geom.Rectangle(30, 30, 300, 540);
this.group = this.physics.add.group({ immovable: true });

Сначала задаются границы мира. Затем создаётся прямоугольная область bounds, внутри которой будут случайным образом размещаться спрайты. Ключевой параметр immovable: true указывает, что тела в этой группе неподвижны. Для таких тел Arcade Physics строит R-Tree, чтобы оптимизировать проверку столкновений с движущимися объектами.

for (let i = 0; i < 50; i++)
{
    const pos = bounds.getRandomPoint();
    this.group.create(pos.x, pos.y, 'chunk');
}

Цикл создаёт 50 спрайтов в случайных точках внутри области bounds и добавляет их в группу. Каждый спрайт автоматически получает статическое физическое тело.

Движущийся объект и регистрация коллайдера

Далее создаётся управляемый игроком объект — ящик (crate).

this.player = this.physics.add.image(600, 300, 'crate');
this.physics.add.collider(this.player, this.group);

Метод this.physics.add.image создаёт спрайт с динамическим физическим телом. Важный момент: движущийся объект (player) находится далеко от группы статиков (координата x=600 против области bounds с x от 30 до 330).

Метод this.physics.add.collider регистрирует коллайдер между игроком и группой. При проверке столкновений на каждом кадре движок, используя R-Tree для группы, быстро поймёт, что игрок находится далеко от всех статичных объектов, и пропустит ресурсоёмкие узкофазные расчёты.

Управление и обновление физики

В методе update() реализовано управление игроком с клавиатуры.

this.player.setVelocity(0);
if (this.cursors.left.isDown) { this.player.setVelocityX(-500); }
else if (this.cursors.right.isDown) { this.player.setVelocityX(500); }
if (this.cursors.up.isDown) { this.player.setVelocityY(-500); }
else if (this.cursors.down.isDown) { this.player.setVelocityY(500); }

Каждый кадр скорость объекта сначала сбрасывается, а затем назначается в зависимости от нажатых клавиш-стрелок. Физический движок Arcade автоматически применяет эту скорость, перемещает тело и проверяет столкновения.

Именно здесь вступает в работу оптимизация: когда игрок движется в пустой части экрана, R-Tree для группы статиков позволяет движку мгновенно заключить, что столкновений быть не может, не перебирая все 50 объектов.

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

Использование R-Tree в Arcade Physics — мощный, но часто незаметный инструмент для поддержания высокой частоты кадров. Он автоматически применяется к неподвижным телам (immovable: true), делая проверку столкновений эффективной. **Идеи для экспериментов:** 1. Увеличьте количество объектов в группе до 500 и понаблюдайте за производительностью. 2. Сделайте группу динамической (уберите immovable), чтобы увидеть, как падает производительность без R-Tree. 3. Распределите статичные объекты по всей сцене и проверьте, как меняется нагрузка, когда игрок находится в их гуще.