О чем этот пример
Разработка игр часто упирается в производительность: как отобразить сотни анимированных объектов без падения FPS? Встроенный в Phaser объект `Blitter` — это секретное оружие для работы с большим количеством спрайтов. Он использует технологию batching, чтобы отрисовывать тысячи элементов за один вызов WebGL. В этой статье мы разберем практический пример, где `Blitter` комбинируется с системой твинов для создания плавной анимации частиц, и научимся отслеживать производительность в реальном времени.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
numbers = [];
frame = 'veg01';
idx = 1;
blitter;
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 ()
{
this.numbers.push(this.add.image(32 + 0 * 50, 742, 'atlas', '0'));
this.numbers.push(this.add.image(32 + 1 * 50, 742, 'atlas', '0'));
this.numbers.push(this.add.image(32 + 2 * 50, 742, 'atlas', '0'));
this.numbers.push(this.add.image(32 + 3 * 50, 742, 'atlas', '0'));
this.numbers.push(this.add.image(32 + 4 * 50, 742, 'atlas', '0'));
this.numbers.push(this.add.image(32 + 5 * 50, 742, 'atlas', '0'));
this.blitter = this.add.blitter(0, 0, 'atlas');
for (let i = 0; i < 32; i++)
{
this.launch(i);
}
this.updateDigits();
}
update ()
{
if (this.input.activePointer.isDown)
{
for (let i = 0; i < 32; i++)
{
this.launch(i);
}
this.updateDigits();
}
}
launch (i)
{
this.idx++;
if (this.idx === 38)
{
this.idx = 1;
}
if (this.idx < 10)
{
this.frame = `veg0${this.idx.toString()}`;
}
else
{
this.frame = `veg${this.idx.toString()}`;
}
const bob = this.blitter.create(i * 32, 0, this.frame);
this.tweens.add({
targets: bob,
duration: 2000,
y: 650,
delay: Math.random() * 2,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true
});
}
updateDigits ()
{
const len = Phaser.Utils.String.Pad(this.blitter.children.list.length.toString(), 6, '0', 1);
this.numbers[0].setFrame(len[0]);
this.numbers[1].setFrame(len[1]);
this.numbers[2].setFrame(len[2]);
this.numbers[3].setFrame(len[3]);
this.numbers[4].setFrame(len[4]);
this.numbers[5].setFrame(len[5]);
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Что такое Blitter и зачем он нужен
Blitter (от «bit block transfer») — это специальный игровой объект в Phaser, оптимизированный для отрисовки множества однотипных спрайтов. В отличие от обычных Image или Sprite, которые требуют отдельных вызовов отрисовки, Blitter собирает все свои дочерние элементы (их называют «bob») в один пакет (batch) и отправляет его в графический процессор за один проход.
Это критически важно для производительности, особенно при работе с частицами, каплями дождя, звёздами на небе или, как в нашем примере, с падающими фруктами. Каждый элемент (bob) внутри Blitter — это просто ссылка на область (frame) в текстуре атласа и координаты для отрисовки.
Создание Blitter в сцене выглядит так:
this.blitter = this.add.blitter(0, 0, 'atlas');
Здесь мы создаём контейнер Blitter, который будет использовать текстуру из загруженного атласа 'atlas'.
Анатомия примера: запуск и анимация частиц
В методе create() создаётся 32 начальных частицы, а в update() — по 32 новых на каждый клик мыши. Ключевой метод launch() отвечает за создание и анимацию каждой частицы.
Сначала он вычисляет имя кадра (frame) из атласа. Атлас в примере содержит кадры с именами от veg01 до veg37. Код циклически перебирает их.
if (this.idx < 10) {
this.frame = `veg0${this.idx.toString()}`;
} else {
this.frame = `veg${this.idx.toString()}`;
}
Затем создаётся сам bob — визуальный элемент внутри Blitter.
const bob = this.blitter.create(i * 32, 0, this.frame);
Параметры: начальная координата X, Y и имя кадра из атласа. После создания к bob сразу применяется твин — плавная анимация движения по оси Y.
this.tweens.add({
targets: bob,
duration: 2000,
y: 650,
delay: Math.random() * 2,
ease: 'Sine.easeInOut',
repeat: -1,
yoyo: true
});
Здесь targets: bob указывает, что анимация применяется к конкретному bob. Параметры repeat: -1 и yoyo: true заставляют анимацию бесконечно повторяться в прямом и обратном направлении, создавая эффект «подпрыгивания». Math.random() в delay добавляет разброс во времени запуска, чтобы частицы двигались не синхронно.
Считываем метрики: счётчик объектов в реальном времени
Визуальный счётчик внизу экрана — не просто украшение. Это инструмент для отслеживания количества активных bob-объектов внутри Blitter. Он демонстрирует, как легко можно создать систему мониторинга прямо в игре.
В create() создаётся массив из шести объектов Image, которые будут отображать цифры.
this.numbers.push(this.add.image(32 + 0 * 50, 742, 'atlas', '0'));
// ... и так еще 5 раз
Каждое изображение использует тот же атлас 'atlas', где цифры от '0' до '9' также являются отдельными кадрами.
Метод updateDigits() обновляет эти цифры.
const len = Phaser.Utils.String.Pad(this.blitter.children.list.length.toString(), 6, '0', 1);
this.numbers[0].setFrame(len[0]);
this.numbers[1].setFrame(len[1]);
// ... и так для всех шести цифр
this.blitter.children.list — это массив всех активных bob-объектов. Его длина преобразуется в строку и дополняется нулями слева до 6 символов с помощью Phaser.Utils.String.Pad. Затем каждый объект Image в массиве numbers получает свой кадр-цифру через метод setFrame(). Таким образом, мы видим точное количество частиц, например, «000032» или «000512».
Оптимизация и практические советы
1. **Атлас текстуры обязателен.** Blitter работает только с одним источником текстур. Все кадры для bob должны находиться в одном атласе, загруженном в preload().
2. **Твины — это безопасно.** Система твинов Phaser (this.tweens.add) оптимизирована для работы с большим количеством целей. Анимировать свойства bob (x, y, alpha) с её помощью — стандартная и эффективная практика.
3. **Управление памятью.** В примере bob создаются, но не уничтожаются. В реальном проекте для частиц, которые исчезают, следует использовать пулы объектов или удалять bob по завершении анимации, чтобы избежать утечки памяти.
4. **Используйте children.list.** Это прямой доступ к массиву всех bob для подсчёта, итерации или массовых операций.
Что попробовать дальше
Комбинация Blitter для эффективного рендеринга и системы твинов для плавной анимации — мощный паттерн в Phaser для создания сложных визуальных эффектов с тысячами объектов. Этот подход лежит в основе систем частиц, фоновых декораций и массовых сцен.
**Идеи для экспериментов:**
1. Измените твин: добавьте анимацию прозрачности (alpha) или масштаба (scaleX, scaleY).
2. Реализуйте удаление bob через 5 секунд после создания, используя setTimeout или событие твина onComplete.
3. Замените реакцию на клик на автоматический спавн частиц с интервалом (setInterval) и посмотрите, при каком их количестве начнутся заметные просадки FPS.
4. Создайте несколько Blitter для разных текстур и управляйте их глубиной (depth), чтобы формировать слои заднего фона.
