О чем этот пример
При разработке игр часто возникает необходимость многократного использования одних и тех же игровых объектов, например, пуль или врагов. Создавать их заново для каждого выстрела — ресурсоемко. Группы 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).
