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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    lastFired = 0;
    cursors;
    stats;
    speed;
    ship;
    bullets;

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

    create ()
    {
        class Bullet extends Phaser.GameObjects.Image
        {
            constructor (scene)
            {
                super(scene, 0, 0, 'bullet');

                this.speed = Phaser.Math.GetSpeed(400, 1);
            }

            fire (x, y)
            {
                this.setPosition(x, y - 50);
            }

            update (time, delta)
            {
                this.y -= this.speed * delta;

                if (this.y < -50)
                {
                    this.setActive(false);
                    this.setVisible(false);
                }
            }
        }

        //  Limited to just 4 objects in the pool, not allowed to grow beyond it
        this.bullets = this.add.group({
            classType: Bullet,
            maxSize: 4,
            runChildUpdate: true
        });

        this.ship = this.add.sprite(400, 500, 'ship').setDepth(1);

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

        this.speed = Phaser.Math.GetSpeed(300, 1);
    }

    update (time, delta)
    {
        if (this.cursors.left.isDown)
        {
            this.ship.x -= this.speed * delta;
        }
        else if (this.cursors.right.isDown)
        {
            this.ship.x += this.speed * delta;
        }

        if (this.cursors.up.isDown && time > this.lastFired)
        {
            const bullet = this.bullets.get();

            if (bullet)
            {
                bullet.fire(this.ship.x, this.ship.y);

                this.lastFired = time + 50;
            }
        }
    }
}

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

const game = new Phaser.Game(config);

Зачем нужны пулы объектов?

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

Пул (или объектный пул) — это заранее созданная коллекция объектов, которые можно активировать и деактивировать. Когда нужна новая пуля, мы не создаем ее, а берем уже существующую неактивную из пула. После использования (например, пуля улетела за экран) мы не удаляем ее, а возвращаем в пул, делая неактивной и невидимой.

Phaser предоставляет для этого мощный инструмент — Phaser.GameObjects.Group.

Создание группы с ограниченным размером

Ключевая настройка пула — maxSize. Она задает жесткий лимит на количество объектов в группе. Группа не будет создавать объектов больше этого значения, даже если все они активны.

В методе create() нашей сцены мы создаем группу для пуль:

this.bullets = this.add.group({
    classType: Bullet,
    maxSize: 4,
    runChildUpdate: true
});

- classType: Bullet: Определяет класс объектов, которые будут храниться в группе. Phaser сам создаст экземпляры. - maxSize: 4: Максимальное количество объектов в группе. Здесь пул ограничен всего 4 пулями. - runChildUpdate: true: Включение автоматического вызова метода update() для каждого активного дочернего объекта в группе на каждом кадре игры.

После создания группа сразу инициализирует 4 объекта класса Bullet, устанавливая их в неактивное состояние (active = false).

Класс Bullet: логика переиспользования

Для работы с пулом наш игровой объект должен уметь переключаться между активным и неактивным состояниями. Создадим пользовательский класс Bullet.

class Bullet extends Phaser.GameObjects.Image
{
    constructor (scene)
    {
        super(scene, 0, 0, 'bullet');
        this.speed = Phaser.Math.GetSpeed(400, 1);
    }

    fire (x, y)
    {
        this.setPosition(x, y - 50);
        this.setActive(true);
        this.setVisible(true);
    }

    update (time, delta)
    {
        this.y -= this.speed * delta;

        if (this.y < -50)
        {
            this.setActive(false);
            this.setVisible(false);
        }
    }
}

- constructor: Создает пулю с текстурой 'bullet' в координатах (0,0). Phaser.Math.GetSpeed(400, 1) конвертирует пиксели в секунду (400) в значение, которое можно умножить на delta для плавного движения независимо от частоты кадров. - fire(x, y): Метод для «выстрела». Устанавливает пулю в позицию корабля (со смещением вверх) и делает ее активной и видимой. - update(time, delta): Двигает пулю вверх. Если пуля улетела за верхнюю границу экрана (y < -50), она деактивируется и становится невидимой, автоматически возвращаясь в пул. Именно вызов setActive(false) сообщает группе, что объект снова доступен для использования.

Извлечение пули из пула и управление кораблем

Основная игровая логика находится в методе update() сцены. Здесь мы обрабатываем ввод и пытаемся «выстрелить».

update (time, delta)
{
    // Движение корабля
    if (this.cursors.left.isDown)
    {
        this.ship.x -= this.speed * delta;
    }
    else if (this.cursors.right.isDown)
    {
        this.ship.x += this.speed * delta;
    }

    // Стрельба с задержкой
    if (this.cursors.up.isDown && time > this.lastFired)
    {
        const bullet = this.bullets.get();

        if (bullet)
        {
            bullet.fire(this.ship.x, this.ship.y);
            this.lastFired = time + 50;
        }
    }
}
- `this.bullets.get()`: Это самый важный вызов. Метод `get()` группы ищет первый неактивный (`active=false`) объект в пуле, активирует его и возвращает. Если все 4 пули в полете (активны), метод вернет `null`.
- `if (bullet)`: Проверка необходима! Если пул пуст (все 4 пули летят), выстрела не произойдет. Игрок физически ощутит ограничение `maxSize`.
- `this.lastFired = time + 50`: Создает задержку в 50 мс между выстрелами, чтобы они не сыпались непрерывным потоком.

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

Конфигурация игры стандартна. Ключевой момент — указание нашего класса Example в качестве сцены.

const config = {
    type: Phaser.WEBGL,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example // Наш класс сцены с логикой пула
};

const game = new Phaser.Game(config);

Этот код создает игровое окно 800x600 с темно-серым фоном и запускает экземпляр нашей сцены, где уже работает пул пуль.

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

Использование групп с maxSize — это эффективный паттерн для управления множеством однотипных объектов в Phaser. Он предотвращает фрагментацию памяти и обеспечивает стабильную производительность. **Идеи для экспериментов:** 1. Увеличьте maxSize до 20 и добавьте автоматическую стрельбу. Следите за FPS в консоли разработчика. 2. Измените логику Bullet.update(): заставьте пули отскакивать от границ экрана и возвращаться в пул только после 3 отскоков. 3. Создайте второй пул для врагов с другим classType и настройте их появление по таймеру. 4. Добавьте в класс Bullet свойство damage и метод hit(), который будет деактивировать пулю при столкновении с врагом (используйте this.physics.add.overlap).