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

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

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

Живой запуск

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

Исходный код


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

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

    create ()
    {
        const graphics = this.add.graphics();
        graphics.fillStyle(0x000044);
        graphics.fillRect(0,140,800,460);

        this.physics.world.setBounds(0, 0, 800, 600);

        this.spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -10, -200);
        this.spriteBounds.y += 100;

        this.group = this.physics.add.group();
        this.group.runChildUpdate = false;

        //  Create 10,000 bodies at a rate of 100 bodies per 500ms
        this.time.addEvent({ delay: 500, callback: this.release, callbackScope: this, repeat: (10000 / 100) - 1 });

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

        this.player = this.physics.add.image(400, 100, 'crate');

        this.player.setImmovable();
        this.player.setCollideWorldBounds(true);

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

        this.text = this.add.text(10, 10, 'Total: 0', { font: '16px Courier', fill: '#ffffff' });
    }

    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);
        }
    }

    release ()
    {
        for (let i = 0; i < 100; i++)
        {
            const pos = Phaser.Geom.Rectangle.Random(this.spriteBounds);

            const block = this.group.create(pos.x, pos.y, 'chunk');

            block.setBounce(1);
            block.setCollideWorldBounds(true);
            block.setVelocity(Phaser.Math.Between(-200, 200), Phaser.Math.Between(-100, -200));
            block.setMaxVelocity(300);
            block.setBlendMode(1);
        }

        this.text.setText(`Total: ${this.group.getLength()}`);
    }
}

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

const game = new Phaser.Game(config);

Настройка физического мира и границ

В методе create() мы задаём границы физического мира и создаём игровую область. Важно: this.physics.world.setBounds определяет, где физические тела могут перемещаться и сталкиваться с краями.

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

this.physics.world.setBounds(0, 0, 800, 600);
this.spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -10, -200);
this.spriteBounds.y += 100;

Групповая физика и управляемый игрок

Физическая группа (this.physics.add.group) — это коллекция тел, которыми движок управляет оптом. Свойство runChildUpdate: false отключает автоматический вызов update() у каждого спрайта, что повышает производительность.

Игрок (player) — это физическое тело (image), которое мы делаем неподвижным (setImmovable) и сталкивающимся с миром. Коллайдер между игроком и группой обеспечивает взаимодействие.

this.group = this.physics.add.group();
this.group.runChildUpdate = false;
this.player = this.physics.add.image(400, 100, 'crate');
this.player.setImmovable();
this.player.setCollideWorldBounds(true);
this.physics.add.collider(this.player, this.group);

Порционное создание объектов с таймером

Чтобы не создавать 10 000 тел одномоментно, используется событие таймера this.time.addEvent. Оно вызывает метод release каждые 500 мс, создавая по 100 частиц за раз. Это снижает нагрузку на процессор при старте сцены.

this.time.addEvent({ delay: 500, callback: this.release, callbackScope: this, repeat: (10000 / 100) - 1 });

Генерация частиц со случайными параметрами

Метод release создаёт частицы внутри spriteBounds. Каждой задаётся случайная позиция, скорость, отскок (bounce: 1) и режим наложения цвета (blendMode). setMaxVelocity ограничивает максимальную скорость, предотвращая неконтролируемое ускорение.

const pos = Phaser.Geom.Rectangle.Random(this.spriteBounds);
const block = this.group.create(pos.x, pos.y, 'chunk');
block.setBounce(1);
block.setCollideWorldBounds(true);
block.setVelocity(Phaser.Math.Between(-200, 200), Phaser.Math.Between(-100, -200));
block.setMaxVelocity(300);
block.setBlendMode(1);

Управление игроком и отображение счётчика

В update() обрабатывается ввод с клавиатуры: стрелки задают скорость игроку через setVelocity. Это стандартный способ управления физическим телом в Arcade.

Счётчик в левом верхнем углу обновляется в release(), показывая текущее количество созданных тел в группе.

if (this.cursors.left.isDown)
{
    this.player.setVelocityX(-500);
}
this.text.setText(`Total: ${this.group.getLength()}`);

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

Ключевые настройки в конфиге: type: Phaser.WEBGL для использования WebGL-рендерера, что критично для отрисовки тысяч спрайтов. В настройках Arcade physics отключено дерево квадрантов (useTree: false), что ускоряет обработку столкновений при большом количестве динамических тел.

physics: {
    default: 'arcade',
    arcade: {
        useTree: false,
        gravity: { y: 100 }
    }
}

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

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