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

При создании игр с богатым фоном, например, платформеров или скролл-шуттеров, часто возникает задача отрисовки тысяч объектов без падения FPS. Обычные спрайты или группы могут не справиться с такой нагрузкой. В этой статье мы разберем пример использования `SpriteGPULayer` — мощного инструмента Phaser для высокопроизводительного рендеринга множества однотипных спрайтов. Вы узнаете, как создать бесконечно скроллящийся фон из сотен ящиков с минимальными затратами ресурсов и добавить к нему визуальные эффекты.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    spriteGPULayer;
    xSpacing = 32;
    ySpacing = 16;
    nextCameraY = 0;

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('crate32', 'assets/sprites/crate32.png');
    }

    create ()
    {
        this.spriteGPULayer = this.add.spriteGPULayer(
            'crate32',
            (1 + this.renderer.width / this.xSpacing) * (2 + this.renderer.height / this.ySpacing)
        );

        for (let y = this.renderer.height + 16; y > -16; y -= this.ySpacing)
        {
            this.createCrateRow(y);
        }

        this.cameras.main.filters.internal.addTiltShift();
    }

    update ()
    {
        this.cameras.main.scrollY -= 1;
        if (this.cameras.main.scrollY < this.nextCameraY)
        {
            this.nextCameraY = this.cameras.main.scrollY - this.ySpacing;
            this.createCrateRow(this.nextCameraY);
        }
    }

    createCrateRow (y)
    {
        for (
            let x = 0;
            x < this.renderer.width + this.xSpacing;
            x += this.xSpacing
        )
        {
            // Ordinarily, we wouldn't want to create new objects for every member.
            // It's more efficient to reuse a single object and update its properties,
            // or use raw data and update the buffer directly.
            // This example is for illustrative purposes only.
            const memberConfig = {
                x: x + Math.random() * 4,
                y: y + Math.random() * 16,
                rotation: (Math.random() - 0.5) * 0.2,
                scaleX: 1 + Math.random() * 0.5,
                scaleY: 1 + Math.random() * 0.5,
                tintTopRight: 0xaabbcc,
                tintBottomLeft: 0xaabbcc,
                tintBottomRight: 0x778899
            };
            this.spriteGPULayer.insertMembers(0, memberConfig);
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 1280,
    height: 720,
    scene: Example,
    backgroundColor: '#202020'
};

const game = new Phaser.Game(config);

Что такое SpriteGPULayer?

SpriteGPULayer — это специальный тип контейнера в Phaser, предназначенный для отрисовки большого количества экземпляров одного спрайта с использованием возможностей GPU. В отличие от обычных спрайтов, которые управляются по отдельности, SpriteGPULayer хранит данные о трансформациях (позиция, поворот, масштаб, цвет) всех своих членов (members) в буфере и отрисовывает их за один или несколько проходов шейдера. Это дает огромный прирост производительности при работе с тысячами объектов.

Ключевой метод для добавления объектов — insertMembers(). В примере слой создается с расчетом на определенное максимальное количество членов, исходя из размеров экрана и шага размещения.

Инициализация слоя и создание первого кадра

В методе create() происходит основная настройка. Сначала создается сам SpriteGPULayer. Ему передается ключ текстуры и начальная емкость — примерное максимальное количество спрайтов, которое может поместиться на экране с учетом смещения.

this.spriteGPULayer = this.add.spriteGPULayer(
    'crate32',
    (1 + this.renderer.width / this.xSpacing) * (2 + this.renderer.height / this.ySpacing)
);

Затем, в цикле for, заполняются начальные ряды ящиков, начиная с нижней части экрана и уходя за его верхнюю границу. Это создает первоначальную "подушку" объектов для скролла.

for (let y = this.renderer.height + 16; y > -16; y -= this.ySpacing)
{
    this.createCrateRow(y);
}

В конце к main камере добавляется фильтр tiltShift, который создает эффект размытия, имитирующий глубину резкости и добавляющий сцене атмосферности.

Логика добавления новых рядов (createCrateRow)

Функция createCrateRow отвечает за создание одного горизонтального ряда ящиков на заданной координате Y. Для каждого потенциального места в ряду (с шагом xSpacing) создается конфигурационный объект memberConfig.

const memberConfig = {
    x: x + Math.random() * 4,
    y: y + Math.random() * 16,
    rotation: (Math.random() - 0.5) * 0.2,
    scaleX: 1 + Math.random() * 0.5,
    scaleY: 1 + Math.random() * 0.5,
    tintTopRight: 0xaabbcc,
    tintBottomLeft: 0xaabbcc,
    tintBottomRight: 0x778899
};

В этом объекте задаются: - **x, y**: Позиция с небольшим случайным смещением для естественности. - **rotation**: Случайный небольшой угол поворота. - **scaleX, scaleY**: Случайный масштаб для визуального разнообразия. - **tint...**: Оттенки цвета для вершин спрайта, создающие сложное окрашивание.

Затем этот конфиг добавляется в слой с помощью insertMembers(0, memberConfig). Первый аргумент `0` означает индекс, куда вставлять члена. В данном случае они добавляются в начало внутреннего массива слоя.

**Важное замечание из кода**: В реальном проекте создание нового объекта memberConfig для каждого члена на каждом кадре неэффективно. Для максимальной производительности следует переиспользовать один объект, обновляя его свойства, или работать с сырыми данными буфера напрямую.

Бесконечный скроллинг и динамическое пополнение

Логика скроллинга реализована в методе update(). Каждый кадр камера немного смещается вверх.

this.cameras.main.scrollY -= 1;

Система отслеживает, насколько далеко мы прокрутили сцену (scrollY). Когда это значение становится меньше заранее вычисленного порога (nextCameraY), это сигнал, что пора добавить новый ряд ящиков вверху, за границей видимой области.

if (this.cameras.main.scrollY < this.nextCameraY)
{
    this.nextCameraY = this.cameras.main.scrollY - this.ySpacing;
    this.createCrateRow(this.nextCameraY);
}

Таким образом, создается иллюзия бесконечного поля ящиков, уходящего вдаль. Старые ряды, уехавшие за нижнюю границу экрана, просто перестают отрисовываться. В этом примере они остаются в памяти слоя. В полноценной игре их стоило бы удалять через метод removeMembers() для экономии памяти.

Что попробовать дальше

SpriteGPULayer — это ключ к созданию сложных, плотных фонов и частиц в Phaser без потери производительности. Основной принцип: один спрайт — множество экземпляров с индивидуальными трансформациями, управляемыми через быстрые буферы GPU. **Идеи для экспериментов:** 1. Замените текстуру ящика на звезду или облако и создайте бесконечный звездный фон или небо. 2. Реализуйте удаление рядов, уехавших за нижнюю границу экрана, с помощью removeMembers(). 3. Динамически меняйте свойства членов (например, tint) в зависимости от положения камеры, чтобы создать градиент цвета или эффект затухания вдали. 4. Используйте разные фильтры камеры (Blur, Glow) вместе с tiltShift для достижения уникального визуального стиля.