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

В играх с множеством объектов, которые постоянно появляются и исчезают (например, снаряды, враги, частицы), создание и уничтожение физических тел на лету может привести к просадкам производительности и сборке мусора. Этот пример демонстрирует продвинутую технику оптимизации: создание пула заранее подготовленных объектов `Phaser.Physics.Arcade.Body` и их повторное использование. Вместо того чтобы каждый раз создавать тело с нуля, мы берём его из пула, настраиваем и возвращаем обратно, когда объект покидает экран. Такой подход значительно снижает нагрузку на процессор и делает игру плавнее.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('player', 'assets/sprites/ship.png');
        this.load.image('ship', 'assets/sprites/bsquadron1.png');
        this.load.image('apple', 'assets/sprites/apple.png');
        this.load.image('beball', 'assets/sprites/beball1.png');
        this.load.image('clown', 'assets/sprites/clown.png');
        this.load.image('ghost', 'assets/sprites/ghost.png');
    }

    create ()
    {
        const player = this.physics.add.image(400, 500, 'player');

        //  This array contains all the enemy sprites which are currently active and moving
        this.active = [];

        //  Create a pool of 100 physics bodies.

        //  To achieve this, we need a dummy Game Object they can pull default values from.

        this.pool = [];

        const dummy = this.add.image();
        const { world } = this.physics;

        for (let i = 0; i < 100; i++)
        {
            const body = new Phaser.Physics.Arcade.Body(world, dummy);

            this.pool.push(body);
        }

        //  Every 250ms we'll release an enemy
        this.time.addEvent({ delay: 250, callback: () => this.releaseEnemy(), loop: true });

        //  Let the player move the ship with the mouse
        this.input.on('pointermove', pointer =>
        {
            player.x = pointer.worldX;
            player.y = pointer.worldY;
        });

        // this.physics.add.collider(sprite, balls);
    }

    update ()
    {
        this.checkEnemyBounds();
    }

    checkEnemyBounds ()
    {
        const { world } = this.physics;

        //  Check which enemies have left the screen
        for (let i = this.active.length - 1; i >= 0; i--)
        {
            const enemy = this.active[i];

            if (enemy.y > 700)
            {
                //  Recycle this body
                const { body } = enemy;

                //  Remove it from the internal world trees
                world.disableBody(body);

                //  Clear the gameObject references
                body.gameObject = undefined;

                //  Put it back into the pool
                this.pool.push(body);

                //  Nuke the sprite
                enemy.body = undefined;
                enemy.destroy();

                //  Remove it from the active array
                this.active.splice(i, 1);

                //  ^^^ technically, you wouldn't ever destroy the sprite, but instead
                //  put them back into their own pools for later re-use, otherwise there's no
                //  point recycling the physics bodies!
            }
        }
    }

    releaseEnemy ()
    {
        const { pool } = this;

        const body = pool.pop();

        const x = Phaser.Math.Between(0, 800);
        const y = Phaser.Math.Between(-1200, 0);
        const frames =[ 'ship', 'apple', 'beball', 'clown', 'ghost' ];

        const enemy = this.add.image(x, y, Phaser.Utils.Array.GetRandom(frames));

        //  Link the sprite to the body
        enemy.body = body;
        body.gameObject = enemy;

        //  We need to do this to give the body the frame size of the sprite
        body.setSize();

        //  Now you could call 'setCircle' etc as required

        //  Insert the body back into the physics world
        this.physics.world.add(body);

        //  Give it some velocity
        body.setVelocity(0, Phaser.Math.Between(200, 400));

        //  Add to our active pool
        this.active.push(enemy);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            debugShowVelocity: false
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Зачем нужен пул тел?

При вызове this.physics.add.image() Phaser создаёт спрайт и привязывает к нему новое физическое тело. Если объект уничтожается (например, враг убит или улетел за экран), его тело тоже удаляется. При активном геймплее это приводит к постоянным операциям выделения и освобождения памяти, что нагружает сборщик мусора и может вызывать рывки в работе игры.

Создание пула из 100 тел один раз при старте сцены решает эту проблему. Мы заранее создаём «заготовки» тел, а в процессе игры только берём их из пула, настраиваем и возвращаем обратно.

Создание пула тел

Ключевой момент — тело (Body) не может существовать само по себе, ему нужен игровой объект (Game Object) для получения размеров по умолчанию. Поэтому мы создаём «пустышку» — спрайт dummy без текстуры, который служит шаблоном.

Затем в цикле создаём новые экземпляры Phaser.Physics.Arcade.Body, передавая им мир физики и объект-шаблон, и складываем в массив pool.

this.pool = [];
const dummy = this.add.image();
const { world } = this.physics;

for (let i = 0; i < 100; i++)
{
    const body = new Phaser.Physics.Arcade.Body(world, dummy);
    this.pool.push(body);
}

Активация тела из пула

Метод releaseEnemy() вызывается каждые 250 мс через this.time.addEvent. Он отвечает за выпуск нового врага на экран.

Сначала мы извлекаем тело из конца массива pool с помощью pop(). Затем создаём спрайт врага со случайным изображением в случайной позиции в верхней части экрана.

Далее происходит связывание: мы присваиваем спрайту извлечённое тело (enemy.body = body), а телу — спрайт (body.gameObject = enemy). Вызов body.setSize() обновляет размеры коллайдера тела в соответствии с размерами кадра спрайта.

Важный шаг — явное добавление тела обратно в мир физики с помощью this.physics.world.add(body). После этого телу задаётся скорость, и спрайт добавляется в массив active для последующего отслеживания.

const body = pool.pop();
const x = Phaser.Math.Between(0, 800);
const y = Phaser.Math.Between(-1200, 0);
const frames =[ 'ship', 'apple', 'beball', 'clown', 'ghost' ];
const enemy = this.add.image(x, y, Phaser.Utils.Array.GetRandom(frames));

enemy.body = body;
body.gameObject = enemy;
body.setSize();
this.physics.world.add(body);
body.setVelocity(0, Phaser.Math.Between(200, 400));
this.active.push(enemy);

Возврат тела в пул

В методе update() каждый кадр вызывается checkEnemyBounds(), который проверяет, не улетели ли враги за нижнюю границу экрана (y > 700).

Для каждого такого врага выполняется процесс «рециклинга»: 1. Получаем ссылку на его физическое тело. 2. Отключаем тело в мире физики с помощью world.disableBody(body). Это удаляет тело из внутренних структур мира (деревьев пространственного разбиения). 3. Очищаем ссылку на игровой объект у тела: body.gameObject = undefined. 4. Возвращаем тело в пул: this.pool.push(body). 5. Уничтожаем спрайт врага. **Важное замечание из кода**: в реальном проекте спрайты тоже следует помещать в пул для повторного использования, иначе экономия на телах теряет смысл. 6. Удаляем спрайт из массива активных врагов.

if (enemy.y > 700)
{
    const { body } = enemy;
    world.disableBody(body);
    body.gameObject = undefined;
    this.pool.push(body);
    enemy.body = undefined;
    enemy.destroy();
    this.active.splice(i, 1);
}

Конфигурация мира и отладка

В конфигурации игры включён Arcade Physics и активирован режим отладки. Это позволяет видеть границы физических тел (хотя отображение векторов скорости отключено debugShowVelocity: false). При использовании пула тел отладка помогает убедиться, что тела корректно активируются, перемещаются и возвращаются в пул.

physics: {
    default: 'arcade',
    arcade: {
        debug: true,
        debugShowVelocity: false
    }
}

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

Использование пула физических тел — это мощный паттерн оптимизации для динамичных игр с большим количеством кратковременных объектов. Он минимизирует нагрузку на сборщик мусора и обеспечивает стабильный FPS. **Идеи для экспериментов:** 1. Реализуйте пул и для спрайтов, чтобы полностью избежать операций создания и уничтожения. 2. Добавьте столкновения между игроком и врагами, используя this.physics.add.collider(). Убедитесь, что переиспользуемые тела корректно работают в системе коллизий. 3. Измените логику: возвращайте тела в пул не при выходе за границы, а при столкновении со снарядом. 4. Протестируйте производительность, сравнив FPS в сцене с пулом и без него при одновременном появлении сотен объектов.