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

В любой стрелялке или аркадной игре управление снарядами — это ключевой элемент геймплея. Создавать и уничтожать сотни объектов в кадре «в лоб» — верный путь к падению производительности. В этой статье мы разберём паттерн, который используют профессиональные разработчики на Phaser: создание пула переиспользуемых объектов с помощью `Phaser.Physics.Arcade.Group`. Вы научитесь создавать умную систему стрельбы, которая не нагружает память и позволяет легко управлять десятками активных снарядов. Мы не будем просто копировать код из примера, а детально разберём архитектуру решения: как работает класс `Bullet`, как группировка объектов упрощает их обновление и переиспользование, и как связать всё это с управлением игрока. Этот подход пригодится не только для пуль, но и для любых часто создаваемых и уничтожаемых объектов: частиц, бонусов, врагов.

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

Живой запуск

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

Исходный код


class Bullet extends Phaser.Physics.Arcade.Sprite
{
    constructor (scene, x, y)
    {
        super(scene, x, y, 'bullet');
    }

    fire (x, y)
    {
        this.body.reset(x, y);

        this.setActive(true);
        this.setVisible(true);

        this.setVelocityY(-300);
    }

    preUpdate (time, delta)
    {
        super.preUpdate(time, delta);

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

class Bullets extends Phaser.Physics.Arcade.Group
{
    constructor (scene)
    {
        super(scene.physics.world, scene);

        this.createMultiple({
            frameQuantity: 5,
            key: 'bullet',
            active: false,
            visible: false,
            classType: Bullet
        });
    }

    fireBullet (x, y)
    {
        const bullet = this.getFirstDead(false);

        if (bullet)
        {
            bullet.fire(x, y);
        }
    }
}

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

        this.bullets;
        this.ship;
    }

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

    create ()
    {
        this.bullets = new Bullets(this);

        this.ship = this.add.image(400, 500, 'ship');

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

            this.ship.x = pointer.x;

        });

        this.input.on('pointerdown', (pointer) =>
        {

            this.bullets.fireBullet(this.ship.x, this.ship.y);

        });
    }
}

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

const game = new Phaser.Game(config);

Архитектура решения: Разделяем ответственность

Код из примера построен по классическому объектно-ориентированному принципу. Вместо того чтобы в одной сцене описывать и логику корабля, и логику сотни пуль, мы выносим её в отдельные классы. Это делает код чище, его легче тестировать и расширять.

Основных класса три: 1. Bullet — отвечает за состояние и поведение одной пули (полёт, «смерть»). 2. Bullets — управляет всей группой пуль как единым целым (создание пула, выдача готовых к стрельбе пуль). 3. Example (наша сцена) — связывает всё вместе, загружает ресурсы и обрабатывает ввод пользователя.

Давайте детально рассмотрим, как устроен каждый из них.

Класс Bullet: Жизненный цикл одного снаряда

Класс Bullet наследуется от Phaser.Physics.Arcade.Sprite. Это даёт ему все свойства спрайта и физического тела Arcade.

Конструктор просто инициализирует спрайт с нужным ключом текстуры. Вся магия происходит в двух методах:

* fire(x, y) — «оживляет» пулю. Метод this.body.reset() перемещает физическое тело пули в заданные координаты, что безопаснее и эффективнее, чем просто менять this.x и this.y. Затем пуля делается активной и видимой, и ей задаётся вертикальная скорость. * preUpdate(time, delta) — автоматически вызывается на каждом кадре игры. Здесь проверяется, улетела ли пуля за верхнюю границу экрана. Если да, она «деактивируется» — становится невидимой и необновляемой, возвращаясь в пул для повторного использования.

fire (x, y)
{
    this.body.reset(x, y);

    this.setActive(true);
    this.setVisible(true);

    this.setVelocityY(-300);
}

preUpdate (time, delta)
{
    super.preUpdate(time, delta);

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

Класс Bullets: Фабрика и менеджер пуль

Bullets наследуется от Phaser.Physics.Arcade.Group. Группа — это мощный контейнер, который оптимизирует работу с коллекцией однотипных физических объектов.

В конструкторе мы сразу создаём пул из нескольких пуль с помощью this.createMultiple(). Ключевые параметры: * frameQuantity: 5 — создаём 5 пуль «про запас». * active: false и visible: false — изначально все пули неактивны и невидимы. * classType: Bullet — указываем, что создавать нужно объекты нашего класса Bullet, а не стандартные спрайты.

Главный метод группы — fireBullet(x, y). Он запрашивает у группы первую неактивную («мёртвую») пулю через this.getFirstDead(false). Если такая пуля найдена в пуле, мы вызываем у неё метод fire() для запуска.

constructor (scene)
{
    super(scene.physics.world, scene);

    this.createMultiple({
        frameQuantity: 5,
        key: 'bullet',
        active: false,
        visible: false,
        classType: Bullet
    });
}

fireBullet (x, y)
{
    const bullet = this.getFirstDead(false);

    if (bullet)
    {
        bullet.fire(x, y);
    }
}

Сцена: Собираем всё воедино

В сцене происходит инициализация и связывание всех компонентов. В методе preload() загружаются изображения.

В create() создаётся экземпляр группы Bullets и спрайт корабля. Затем настраиваются обработчики ввода: * При движении указателя (pointermove) корабль следует за ним по оси X. * При клике (pointerdown) вызывается this.bullets.fireBullet(), который пытается выстрелить пулей из позиции корабля.

Обратите внимание на конфигурацию игры. Важный параметр physics.default: 'arcade' включает физический движок Arcade, который необходим для работы групп и физических тел.

create ()
{
    this.bullets = new Bullets(this);

    this.ship = this.add.image(400, 500, 'ship');

    this.input.on('pointermove', (pointer) =>
    {
        this.ship.x = pointer.x;
    });

    this.input.on('pointerdown', (pointer) =>
    {
        this.bullets.fireBullet(this.ship.x, this.ship.y);
    });
}
const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            gravity: { y: 0 }
        }
    },
    scene: Example
};

Почему это эффективно? Принцип Object Pool

Представьте, что игрок делает 100 выстрелов. Без пула было бы создано 100 объектов Bullet, а потом, когда они улетели, они бы просто уничтожались сборщиком мусора. Создание и удаление объектов в JavaScript — дорогая операция, которая может вызывать «подтормаживания».

Наш подход реализует паттерн **Object Pool**. Мы создаём ограниченное количество объектов (например, 5) заранее. Когда нужно «выстрелить», мы не создаём новый объект, а берём уже существующий неактивный из пула, «перезаряжаем» его и запускаем. Когда пуля «умирает» (улетает за экран), мы не удаляем её, а просто деактивируем и возвращаем в пул.

Это сводит нагрузку от создания объектов к минимуму и полностью исключает сборку мусора для пуль во время игрового процесса, что критически важно для плавности игры.

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

Использование Phaser.Physics.Arcade.Group в связке с кастомным классом для объектов — это профессиональный и эффективный способ управления множеством однотипных сущностей в игре. Вы получаете контроль, производительность и чистую архитектуру кода. **Идеи для экспериментов:** 1. Добавьте врагов и проверку столкновений пуль с ними через this.physics.add.overlap(). При попадании «убивайте» и пулю, и врага. 2. Реализуйте разные типы пуль (например, медленные, но мощные), создав несколько классов (LaserBullet, PlasmaBullet) и несколько групп для них. 3. Сделайте автоматическую стрельбу при зажатой кнопке мыши, используя таймер или проверку в update(). 4. Поэкспериментируйте с размером пула. Что будет, если создать всего 3 пули, а игрок стреляет чаще? Добавьте логику для динамического расширения пула, если все пули активны.