О чем этот пример
При создании игр с большим количеством анимированных объектов, таких как частицы, враги или эффекты, производительность может резко упасть. Классический подход с созданием сотен отдельных `Sprite` объектов быстро исчерпывает ресурсы. В этой статье мы рассмотрим мощный и пока малоизвестный инструмент Phaser 3 — `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.atlas('sprites', 'assets/atlas/tp3test.png', 'assets/atlas/tp3test.json');
}
create ()
{
const count = 1024;
const animConfig = {
key: 'duoAnim',
frames: [
{
key: 'sprites',
frame: 'asuna_by_vali233.png'
},
{
key: 'sprites',
frame: 'kirito_by_vali233.png'
}
],
frameRate: 2
};
const anim = this.anims.create(animConfig);
const layer = this.add.spriteGPULayer('sprites', count);
layer.setAnimations([anim]);
console.log(layer);
const template = {
// Animation can be set as a key...
// animation: 'duoAnim',
// ... or as a MemberAnimation.
animation: {
base: 'duoAnim',
duration: 1000,
delay: 0
},
x: {
base: 0,
ease: 'Linear',
amplitude: -100,
duration: 500
},
y: {
base: 0,
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
};
for (let i = 0; i < count; i++)
{
const phase = Math.random() * 1000;
template.animation.delay = phase;
template.x.base = Math.random() * 800 + 50;
template.y.base = 600 - Math.random() * 300;
template.x.delay = phase;
template.y.delay = phase;
template.rotation.delay = phase;
template.scaleX.delay = phase;
template.scaleY.delay = phase;
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 — это специализированный объект-контейнер для спрайтов, оптимизированный для отрисовки огромного количества графически идентичных объектов. В отличие от обычных спрайтов, которые являются независимыми игровыми объектами, спрайты в этом слое (members) управляются единым набором инструкций, отправляемых на GPU.
Основная идея: вы создаёте один шаблон (template) для анимации и трансформаций (позиция, вращение, масштаб), а затем создаёте тысячи его экземпляров, варьируя лишь начальные параметры, такие как задержка (delay) или базовая позиция. Вся анимация и движение рассчитываются в шейдерах видеокарты, что освобождает центральный процессор.
const layer = this.add.spriteGPULayer('sprites', count);
В этой строке создаётся сам слой. Первый аргумент — ключ атласа, второй — максимальное количество спрайтов, которое слой может содержать (в примере — 1024).
Подготовка анимации для слоя
Прежде чем добавлять спрайты в слой, нужно определить, как они будут анимированы. Анимация создаётся стандартным способом через менеджер анимаций this.anims. Однако для SpriteGPULayer её необходимо явно передать в слой с помощью метода setAnimations().
const animConfig = {
key: 'duoAnim',
frames: [
{ key: 'sprites', frame: 'asuna_by_vali233.png' },
{ key: 'sprites', frame: 'kirito_by_vali233.png' }
],
frameRate: 2
};
const anim = this.anims.create(animConfig);
layer.setAnimations([anim]);
Важно: метод setAnimations() принимает массив созданных анимаций. В дальнейшем к ним можно обращаться по ключу (в данном случае 'duoAnim').
Создание шаблона для спрайтов
Сердце SpriteGPULayer — это шаблон (template), объект конфигурации, описывающий все свойства каждого спрайта в слое. Шаблон определяет не статические значения, а параметры для циклической анимации каждого свойства.
const template = {
animation: {
base: 'duoAnim', // Ключ анимации
duration: 1000, // Полный цикл анимации в мс
delay: 0 // Начальная задержка
},
x: {
base: 0, // Базовое значение X
ease: 'Linear', // Функция плавности
amplitude: -100, // Амплитуда изменения (будет добавлена к base)
duration: 500 // Длительность цикла движения по X
},
// ... аналогично для y, rotation, scaleX, scaleY
originY: 1 // Статическое свойство: точка вращения Y (1 = нижний край)
};
Каждое свойство (кроме статических, как originY) может быть объектом, описывающим циклическое изменение. Свойство base — это центральное значение, вокруг которого с заданной amplitude (амплитудой) и duration (длительностью) будет происходить колебание по закону, заданному ease. Эффект похож на непрерывно воспроизводимый Tween.
Массовое добавление спрайтов с вариациями
Когда шаблон готов, мы можем заполнить слой спрайтами. Для каждого спрайта мы копируем шаблон и модифицируем в нём несколько ключевых параметров, чтобы создать разнообразие.
for (let i = 0; i < count; i++) {
const phase = Math.random() * 1000;
template.animation.delay = phase;
template.x.base = Math.random() * 800 + 50;
template.y.base = 600 - Math.random() * 300;
template.x.delay = phase;
template.y.delay = phase;
// ... задаём задержку для rotation и scale
layer.addMember(template);
}
Ключевая хитрость — переменная phase (фаза). Это случайная задержка от 0 до 1000 мс, которая присваивается всем анимируемым свойствам и самой анимации кадров. В результате каждый спрайт начинает свой анимационный цикл в случайный момент времени, создавая впечатление живого, несинхронного движения массы объектов. Базовые координаты x.base и y.base также задаются случайно, распределяя спрайты по сцене.
Важные технические детали и ограничения
1. **Тип рендерера:** Для работы SpriteGPULayer необходим WebGL-рендерер (type: Phaser.WEBGL в конфиге). На Canvas он не работает.
2. **Единый атлас:** Все спрайты в одном слое должны использовать текстуры из одного и того же атласа (или изображения), который был передан при создании слоя.
3. **Однотипная анимация:** Хотя можно передать несколько анимаций в setAnimations(), все спрайты в слое будут использовать одну и ту же модель данных из шаблона. Разнообразить можно только параметры (задержку, амплитуду), но не структуру анимации (например, количество кадров).
4. **Производительность vs. Гибкость:** Вы получаете феноменальную производительность ценой гибкости. Вы не можете, например, в реальном времени менять кадр анимации у одного конкретного спрайта в толпе стандартным способом. Управление идёт через общие параметры слоя.
Метод layer.addMember() принимает готовый объект конфигурации и возвращает его же, но уже с уникальным идентификатором (id), который можно использовать для последующего обновления или удаления этого конкретного спрайта из слоя.
Что попробовать дальше
SpriteGPULayer — это мощное оружие для создания масштабных визуальных эффектов, где важна масса однотипных объектов: стаи птиц, рои частиц, поля травы, танцующая толпа или фоновые астероиды. Он радикально снижает нагрузку на CPU, передавая работу на GPU.
**Идеи для экспериментов:**
1. Измените функцию ease в шаблоне на 'Sine.easeInOut' или 'Bounce.easeOut' и наблюдайте, как меняется характер движения.
2. Свяжите амплитуду колебания масштаба (scaleX.amplitude) со случайной базовой позицией y.base, чтобы спрайты внизу экрана колебались сильнее, чем вверху.
3. Попробуйте динамически обновлять свойства слоя, например, постепенно увеличивая amplitude для всех спрайтов по нажатию клавиши, используя методы управления слоем.
