О чем этот пример
При создании игр с большим количеством объектов на экране, таких как стратегии или симуляторы, производительность может стать серьёзной проблемой. Классический подход с созданием сотен отдельных игровых объектов (Sprite) быстро приведёт к падению FPS. В этой статье мы разберём пример использования системы `Blitter` в Phaser — мощного инструмента для пакетного (batch) рендеринга множества одинаковых или похожих спрайтов. Вы научитесь создавать и эффективно управлять сеткой из 10 000 объектов без потери производительности.
Версия 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.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
}
create ()
{
const blitter = this.add.blitter(0, 0, 'bobs');
// Create a load of bobs aligned in a grid
for (let y = 0; y < 100; y++)
{
for (let x = 0; x < 100; x++)
{
blitter.create(x * 32, y * 32, Phaser.Math.Between(0, 399));
}
}
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
}
update (time, delta)
{
this.controls.update(delta);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Что такое Blitter и зачем он нужен?
Blitter (от "bit block transfer") — это специальный игровой объект в Phaser, предназначенный для высокопроизводительного отображения множества спрайтов из одного и того же источника (текстуры или атласа). В отличие от обычного Sprite, который является самостоятельным объектом с физикой, анимацией и событиями, Blitter работает с "бобами" (bobs) — лёгкими инстансами, которые по сути являются инструкциями для отрисовки определённого кадра из текстуры в конкретной позиции.
Основная идея в том, что один Blitter управляет рендерингом тысяч таких инструкций за один draw call, что кардинально снижает нагрузку на GPU. Это идеально для статичных или редко меняющихся элементов: частиц, фоновых тайлов, звёзд на небе или, как в нашем примере, большой сетки спрайтов.
Разбор кода: Создание Blitter и заполнение сетки
В методе preload загружается спрайтшит — изображение, содержащее 400 отдельных кадров (frame) размером 32x32 пикселя.
this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
В методе create создаётся основной объект Blitter в точке (0,0), который будет использовать загруженный спрайтшит 'bobs'.
const blitter = this.add.blitter(0, 0, 'bobs');
Затем два вложенных цикла создают сетку 100x100, то есть 10 000 "бобов". Для каждого элемента вызывается метод blitter.create(x, y, frame). Ключевой момент — третий аргумент, определяющий, какой кадр из спрайтшита будет отрисован. В примере это случайное число от 0 до 399, что даёт разнообразную картинку.
for (let y = 0; y < 100; y++) {
for (let x = 0; x < 100; x++) {
blitter.create(x * 32, y * 32, Phaser.Math.Between(0, 399));
}
}
Управление камерой с помощью SmoothedKeyControl
Поскольку мир теперь огромен (3200x3200 пикселей), нам необходим способ навигации. Phaser предоставляет для этого удобный класс Phaser.Cameras.Controls.SmoothedKeyControl.
Сначала создаётся объект конфигурации controlConfig. В нём указывается:
* camera: Какая камера будет управляться (основная this.cameras.main).
* Ключи для движения (стрелки) и зума (Q/E).
* Параметры плавности: acceleration (ускорение), drag (сопротивление) и maxSpeed (максимальная скорость). Эти значения подобраны для плавного, "инерционного" движения.
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
acceleration: 0.06,
drag: 0.0005,
maxSpeed: 1.0
};
Затем создаётся сам контрол и сохраняется в свойство сцены. Его необходимо обновлять каждый кадр в методе update, передавая дельту времени для корректного расчёта движения.
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
update (time, delta) {
this.controls.update(delta);
}
Практические аспекты и производительность
Почему этот пример работает так быстро? Всё дело в архитектуре рендеринга WebGL. Blitter использует технологию batching — он собирает все данные о позициях и кадрах 10 000 "бобов" в один большой буфер и отправляет его на видеокарту за один раз. Если бы мы создали 10 000 объектов через this.add.sprite(), каждый из них был бы отдельным draw call с накладными расходами, что привело бы к катастрофическому падению производительности.
Важные ограничения и особенности Blitter:
1. **Нет встроенной физики или коллизий.** "Бобы" — это просто отрисовываемые изображения.
2. **Анимация возможна, но требует ручного управления.** Вы можете в update менять кадр у каждого "боба" через свойство frame.
3. **Идеальный use case:** статичный или малоподвижный фон, системы частиц (дождь, снег, звёзды), тайловые карты для больших миров, где каждый тайл — не отдельный игровой объект.
Для динамических объектов, которым нужны взаимодействия, всё ещё используйте Sprite или Image.
Что попробовать дальше
Blitter — это мощное и часто недооценённое оружие в арсенале разработчика игр на Phaser для задач, требующих отрисовки огромного количества визуально похожих элементов. Он позволяет легко преодолеть ограничения производительности, связанные с количеством draw calls.
**Идеи для экспериментов:**
1. Замените случайный выбор кадра на вычисляемый (например, на основе шума Перлина), чтобы создать псевдослучайный ландшафт.
2. Реализуйте простую анимацию, перебирая кадры у всех или некоторых "бобов" в цикле update.
3. Добавьте интерактивности: используйте this.input.on('pointermove', ...) для изменения кадра "бобов" под курсором мыши, создавая эффект "растворения" или смены текстуры.
