О чем этот пример

Визуализация множества анимированных объектов — частая проблема в игровой разработке. Рендеринг каждого спрайта по отдельности съедает драгоценные миллисекунды на каждом кадре. В этой статье мы разберем пример из официального репозитория 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 для вершин, чтобы создавать плавные цветовые градиенты или эффекты растворения для каждого спрайта.