О чем этот пример
В любой стрелялке или аркадной игре управление снарядами — это ключевой элемент геймплея. Создавать и уничтожать сотни объектов в кадре «в лоб» — верный путь к падению производительности. В этой статье мы разберём паттерн, который используют профессиональные разработчики на 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 пули, а игрок стреляет чаще? Добавьте логику для динамического расширения пула, если все пули активны.
