О чем этот пример
Визуализация множества анимированных объектов — частая проблема в игровой разработке. Рендеринг каждого спрайта по отдельности съедает драгоценные миллисекунды на каждом кадре. В этой статье мы разберем пример из официального репозитория Phaser, который показывает, как с помощью `SpriteGPULayer` можно создавать сложные, анимированные толпы из тысяч спрайтов, не теряя в производительности. Этот подход переносит вычисления анимации и трансформаций на GPU, что делает его незаменимым для создания массовых сцен, эффектов частиц или фоновой анимации.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bunny', 'assets/sprites/bunny.png');
}
create ()
{
const bunny = this.add.image(250, 300, 'bunny');
const count = 1024;
const layer = this.add.spriteGPULayer('bunny', 1 + count);
const template = {
x: {
base: 550,
ease: 'Linear',
amplitude: -100,
duration: 500
},
y: {
base: 450,
ease: 'Quad.easeOut',
amplitude: -100,
duration: 250
},
rotation: {
base: -0.25,
ease: 'Linear',
amplitude: 0.5,
duration: 500
},
scaleX: {
base: 1.1,
ease: 'Cubic.easeOut',
amplitude: -0.1,
duration: 250
},
scaleY: {
base: 0.8,
ease: 'Cubic.easeOut',
amplitude: 0.2,
duration: 250
},
originY: 1,
tintBlend: {
base: 1,
ease: 'Quad.easeOut',
amplitude: -1,
duration: 500,
delay: -250
},
tintTopLeft: 0xff0000,
tintBottomLeft: 0x00ff00,
tintTopRight: 0x0000ff,
alphaBottomLeft: 0,
alphaBottomRight: 0,
};
layer.addMember(template);
for (let i = 0; i < count; i++)
{
const phase = Math.random() * 1000;
template.x.base = Math.random() * 800;
template.y.base = 600 - Math.random() * 100;
template.x.delay = phase;
template.y.delay = phase;
template.rotation.delay = phase;
template.scaleX.delay = phase;
template.scaleY.delay = phase;
template.tintBlend.delay = phase - 250;
layer.addMember(template);
}
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example,
backgroundColor: '#808080'
};
const game = new Phaser.Game(config);
Что такое SpriteGPULayer и зачем он нужен
SpriteGPULayer — это специальный тип игрового объекта в Phaser, предназначенный для эффективного рендеринга множества спрайтов, использующих одно и то же текстуру. Вместо того чтобы создавать сотни или тысячи отдельных объектов Image или Sprite и управлять ими через основной игровой цикл, вы создаете один слой. Этот слой работает как шаблонный рендерер: он берет описание одного спрайта (позиция, поворот, масштаб, цвет) и размножает его с вариациями, выполняя все вычисления анимации непосредственно на графическом процессоре.
Такой подход кардинально снижает нагрузку на CPU, потому что логика обновления позиций, поворотов и других свойств для каждого спрайта не выполняется в JavaScript на каждом кадре. Вся анимация описывается декларативно один раз, а затем GPU самостоятельно интерполирует значения между ключевыми состояниями.
Разбираем структуру шаблона (Template)
Сердце SpriteGPULayer — это объект-шаблон, который описывает все свойства и анимации для одного экземпляра спрайта. Давайте посмотрим на его структуру из примера.
const template = {
x: {
base: 550,
ease: 'Linear',
amplitude: -100,
duration: 500
},
// ... другие свойства
};
Каждое свойство (например, `x,y,rotation`) может быть либо статическим числом, либо объектом конфигурации анимации. В объекте анимации:
- base: Базовое значение свойства.
- amplitude: Амплитуда изменения. Финальное значение будет base + amplitude.
- duration: Длительность полного цикла анимации в миллисекундах.
- ease: Функция плавности (easing) для анимации. Используются строковые имена из API Phaser, например, 'Linear', 'Quad.easeOut'.
- delay: Задержка перед началом анимации для данного экземпляра. Это ключ для создания волнообразных или разрозненных эффектов.
Также в шаблоне можно задавать свойства отрисовки, уникальные для SpriteGPULayer, такие как раздельное окрашивание вершин (tintTopLeft, tintBottomLeft и т.д.) и прозрачность (alphaBottomLeft).
Создание слоя и добавление членов
Сначала создается сам слой. Ему передается текстура и максимальное количество спрайтов, которые он может содержать.
const layer = this.add.spriteGPULayer('bunny', 1 + count);
Первый спрайт (в нашем случае — одинокий заяц в углу) добавляется как член слоя с помощью addMember. Это необходимо, чтобы инициализировать слой с данными шаблона.
layer.addMember(template);
Затем в цикле создается основная масса спрайтов. Для каждого нового экземпляра мы слегка модифицируем шаблон, задавая случайные базовые координаты и, что самое важное, случайную задержку (phase) для всех анимируемых свойств.
for (let i = 0; i < count; i++)
{
const phase = Math.random() * 1000;
template.x.base = Math.random() * 800;
template.y.base = 600 - Math.random() * 100;
template.x.delay = phase;
// ... задаем задержку для других свойств
layer.addMember(template);
}
Каждый вызов addMember не создает новый объект в памяти JavaScript. Он лишь добавляет в буфер GPU новый набор данных, основанный на текущем состоянии объекта template. Именно поэтому так важно последовательно менять свойства в template перед каждым добавлением. В результате мы получаем 1024 спрайта, которые начинают свою идентичную анимацию в разное время, создавая впечатление непрерывного, живого движения.
Ключевые преимущества и ограничения подхода
**Преимущества:**
1. **Высочайшая производительность:** Анимация тысяч объектов без нагрузки на CPU.
2. **Декларативность:** Анимация описывается данными, а не кодом обновления в update().
3. **Эффективность по памяти:** Используется одна текстура и минимальные структуры данных на стороне CPU.
**Ограничения, которые важно понимать:**
1. **Динамическое взаимодействие:** Спрайты внутри SpriteGPULayer не являются самостоятельными игровыми объектами. Вы не можете привязать к ним физические тела, обрабатывать клики мыши на отдельных спрайтах или динамически менять их текстуру после создания слоя.
2. **Статичность анимации:** Паттерн анимации (длительность, easing, амплитуда) задается при создании и не может быть изменен «на лету» для отдельных экземпляров. Можно управлять только временем их старта через delay.
3. **Однотипность:** Все спрайты в слое должны использовать одну текстуру (или атлас).
Это делает SpriteGPULayer идеальным решением для фоновых элементов, декораций, невзаимодействующих толп, сложных эффектов частиц (снег, дождь, звезды) — всего, что требует множества визуально похожих, анимированных объектов.
Что попробовать дальше
SpriteGPULayer — это мощный инструмент для оптимизации, открывающий дверь к созданию богатых и плотных визуальных миров без ущерба для частоты кадров. Он перекладывает тяжелую работу с процессора на графический ускоритель, следуя современным подходам в игровой графике.
**Идеи для экспериментов:**
1. Замените простую текстуру зайца на спрайт из атласа и создайте анимированную толпу персонажей.
2. Поэкспериментируйте с функциями плавности (ease). Попробуйте 'Bounce.easeOut' для масштаба или 'Sine.easeInOut' для движения.
3. Создайте эффект «пульсирующей» волны, задавая задержку (delay) не случайно, а вычисляя ее на основе расстояния от центра экрана.
4. Используйте свойства tint и alpha для вершин, чтобы создавать плавные цветовые градиенты или эффекты растворения для каждого спрайта.
