О чем этот пример
При создании игр с большим количеством движущихся объектов, например, частиц или врагов, производительность становится ключевым фактором. Встроенный контейнер `Blitter` в Phaser позволяет отрисовывать сотни и тысячи спрайтов с минимальными накладными расходами, используя преимущества пакетного рендеринга. В этой статье мы разберем практический пример бенчмарка, демонстрирующего работу с `Blitter`, и объясним, почему этот подход эффективнее создания отдельных игровых объектов для массовых элементов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
let blitter;
let gravity = 0.5;
let idx = 1;
let frame = 'veg01';
let numbers = [];
let iter = 0;
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('atlas', 'assets/tests/fruit/veg.png', 'assets/tests/fruit/veg.json');
}
create ()
{
numbers.push(this.add.image(32 + 0 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 1 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 2 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 3 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 4 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 5 * 50, 742, 'atlas', '0'));
numbers.push(this.add.image(32 + 6 * 50, 742, 'atlas', '0'));
blitter = this.add.blitter(0, 0, 'atlas');
for (var i = 0; i < 50; ++i)
{
this.launch();
}
this.updateDigits();
}
update ()
{
if (this.input.activePointer.isDown)
{
for (var i = 0; i < 50; ++i)
{
this.launch();
}
this.updateDigits();
}
for (var index = 0, length = blitter.children.list.length; index < length; ++index)
{
var bob = blitter.children.list[index];
bob.data.vy += gravity;
bob.y += bob.data.vy;
bob.x += bob.data.vx;
if (bob.x > 1024)
{
bob.x = 1024;
bob.data.vx *= -bob.data.bounce;
}
else if (bob.x < 0)
{
bob.x = 0;
bob.data.vx *= -bob.data.bounce;
}
if (bob.y > 684)
{
bob.y = 684;
bob.data.vy *= -bob.data.bounce;
}
}
// this.cameras.main.scrollX = Math.sin(iter) * 200;
// iter += 0.01;
}
launch ()
{
idx++;
if (idx === 38)
{
idx = 1;
}
if (idx < 10)
{
frame = 'veg0' + idx.toString();
}
else
{
frame = 'veg' + idx.toString();
}
var bob = blitter.create(0, 0, frame);
bob.data.vx = Math.random() * 10;
bob.data.vy = Math.random() * 10;
bob.data.bounce = 0.8 + (Math.random() * 0.3);
}
updateDigits ()
{
var len = Phaser.Utils.String.Pad(blitter.children.list.length.toString(), 7, '0', 1);
numbers[0].setFrame(len[0]);
numbers[1].setFrame(len[1]);
numbers[2].setFrame(len[2]);
numbers[3].setFrame(len[3]);
numbers[4].setFrame(len[4]);
numbers[5].setFrame(len[5]);
numbers[6].setFrame(len[6]);
}
}
const config = {
type: Phaser.CANVAS,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Что такое Blitter и зачем он нужен?
Blitter (от "bit block transfer") — это специализированный игровой объект в Phaser, предназначенный для высокопроизводительного отображения множества спрайтов из одного атласа текстур. В отличие от обычных Image или Sprite, которые являются самостоятельными объектами с собственными трансформациями и компонентами, Blitter работает с легковесными сущностями Bob. Эти Bob — это, по сути, инструкции для отрисовки (позиция X/Y и кадр текстуры), которые рендерятся за один вызов отрисовки (draw call). Это резко снижает нагрузку на CPU и GPU при работе с большим количеством визуально однотипных объектов, таких как частицы, звёзды на фоне или падающие листья.
В примере создается один Blitter, который затем управляет сотнями "частиц"-овощей.
Инициализация сцены и создание Blitter
В методе create() происходит базовая настройка сцены. Сначала создается массив numbers — это семь объектов Image, которые будут отображать счетчик количества активных частиц. Они создаются стандартным способом и к механике Blitter не относятся.
Затем создается сам объект Blitter. Он будет служить контейнером для всех частиц.
blitter = this.add.blitter(0, 0, 'atlas');
Сразу после создания в цикле запускается 50 частиц с помощью метода launch(), и обновляется цифровой счетчик.
Создание и управление частицами (Bob)
Метод launch() отвечает за создание и начальную настройку каждой частицы. Ключевой метод — blitter.create(), который добавляет новый объект Bob в Blitter.
var bob = blitter.create(0, 0, frame);
Bob имеет свойство data, которое используется как удобное хранилище для пользовательских данных. В данном примере в него записываются физические свойства частицы: скорость по осям и коэффициент отскока.
bob.data.vx = Math.random() * 10;
bob.data.vy = Math.random() * 10;
bob.data.bounce = 0.8 + (Math.random() * 0.3);
Это отличный пример того, как Blitter берет на себя только отрисовку, а всю игровую логику (физику, состояние) разработчик реализует самостоятельно вручную, что дает полный контроль и максимальную производительность.
Кастомная физика в игровом цикле
Вся физика движения и отскоков реализована вручную в методе update(). Здесь нет встроенного физического движка Phaser. Цикл проходит по всем Bob в списке blitter.children.list и обновляет их состояние.
for (var index = 0, length = blitter.children.list.length; index < length; ++index)
{
var bob = blitter.children.list[index];
bob.data.vy += gravity; // Применяем гравитацию
bob.y += bob.data.vy; // Обновляем позицию
bob.x += bob.data.vx;
// ... проверка границ и отскок
}
При клике мыши создается еще 50 частиц, что позволяет быстро наращивать их количество для проверки производительности. Важно: изменение координат bob.x и bob.y напрямую влияет на его отрисовку в следующем кадре.
Визуализация счетчика частиц
Чтобы наглядно демонстрировать количество активных Bob, в примере используется отдельный механизм отображения цифр. Метод updateDigits() форматирует текущее количество частиц в строку из 7 цифр с ведущими нулями с помощью утилиты Phaser.Utils.String.Pad.
var len = Phaser.Utils.String.Pad(blitter.children.list.length.toString(), 7, '0', 1);
Затем каждая цифра (от 0 до 9) соответствует определенному кадру (frame) в текстовом атласе. Для обновления изображения цифры используется метод setFrame() у заранее созданных объектов Image.
numbers[0].setFrame(len[0]);
Этот подход показывает, как можно комбинировать высокопроизводительный Blitter с обычными игровыми объектами для UI.
Что попробовать дальше
Blitter — это мощный инструмент для ситуаций, где критически важна производительность при отрисовке множества однотипных объектов. Он перекладывает логику обновления на разработчика, но взамен дает почти неограниченную производительность по количеству спрайтов. Для экспериментов попробуйте: изменить гравитацию и коэффициенты отскока, добавить ветер или другие силы; реализовать столкновения между Bob; использовать Blitter для отрисовки тайловой карты или большого количества врагов; активировать закомментированный код с this.cameras.main.scrollX, чтобы проверить, как Blitter ведет себя при движении камеры.
