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

Когда на экране много спрайтов с разными текстурами, движку приходится постоянно переключаться между ними, что снижает производительность. Пример из тестов Phaser демонстрирует, как WebGL-рендерер группирует объекты по текстурам для минимизации переключений, что критично для поддержания высокого FPS в играх с большим количеством визуальных элементов. Понимание этого механизма позволяет осознанно подходить к организации ресурсов и рендеринга.

Версия 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');
        this.load.image('block', 'assets/sprites/block.png');
        this.load.image('brain', 'assets/sprites/brain.png');
        this.load.image('apple', 'assets/sprites/apple.png');
        this.load.image('ball', 'assets/sprites/aqua_ball.png');
    }

    create ()
    {
        this.add.sprite(200, 300, 'bunny').setName('bunny1');
        this.add.sprite(400, 300, 'bunny').setName('bunny2');
        this.add.sprite(600, 300, 'bunny').setName('bunny3');

        this.add.rectangle(400, 100, 500, 16, 0x6666ff);

        this.add.sprite(200, 300, 'brain').setName('brain1');
        this.add.sprite(400, 300, 'brain').setName('brain2');
        this.add.sprite(600, 300, 'brain').setName('brain3');

        this.add.circle(400, 100, 64, 0x9966ff);

        this.add.sprite(200, 300, 'block').setName('block1');
        this.add.sprite(400, 300, 'block').setName('block2');
        this.add.sprite(600, 300, 'block').setName('block3');

        this.add.text(400, 100, 'Multi Texturing').setOrigin(0.5);

        this.add.sprite(200, 300, 'apple').setName('apple1');
        this.add.sprite(400, 300, 'apple').setName('apple2');
        this.add.sprite(600, 300, 'apple').setName('apple3');

        this.add.sprite(200, 300, 'ball').setName('ball1');
        this.add.sprite(400, 300, 'ball').setName('ball2');
        this.add.sprite(600, 300, 'ball').setName('ball3');
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Суть проблемы: переключение текстур

В классическом рендеринге каждый спрайт рисуется отдельным вызовом. Если следующий спрайт использует другую текстуру, графическому конвейеру нужно сделать переключение (texture bind). Это дорогая операция.

WebGL-рендерер Phaser решает эту проблему, сортируя объекты для отрисовки не только по глубине, но и по используемой текстуре. Это позволяет нарисовать все объекты с одной текстурой за один проход, минимизируя переключения.

Пример наглядно это демонстрирует, размещая группы спрайтов с разными текстурами в одном месте.

Разбор примера: загрузка и создание объектов

В методе preload() загружаются пять различных изображений. Обратите внимание на использование setBaseURL — это удобно для работы с ресурсами из удаленного репозитория.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bunny', 'assets/sprites/bunny.png');
this.load.image('block', 'assets/sprites/block.png');
this.load.image('brain', 'assets/sprites/brain.png');
this.load.image('apple', 'assets/sprites/apple.png');
this.load.image('ball', 'assets/sprites/aqua_ball.png');

В create() создаются спрайты и простые графические объекты. Ключевой момент: спрайты с одной текстурой (например, три кролика 'bunny') создаются не подряд, а перемежаются объектами с другими текстурами и графическими примитивами.

this.add.sprite(200, 300, 'bunny').setName('bunny1');
this.add.sprite(400, 300, 'bunny').setName('bunny2');
this.add.sprite(600, 300, 'bunny').setName('bunny3');

this.add.rectangle(400, 100, 500, 16, 0x6666ff);

this.add.sprite(200, 300, 'brain').setName('brain1');
this.add.sprite(400, 300, 'brain').setName('brain2');
this.add.sprite(600, 300, 'brain').setName('brain3');

Метод setName() присваивает объектам уникальные имена, что полезно для отладки, но не влияет на логику рендеринга.

Примитивы и текст: как они влияют на батчинг

Помимо спрайтов, в сцене создаются прямоугольник (add.rectangle), круг (add.circle) и текст (add.text).

this.add.rectangle(400, 100, 500, 16, 0x6666ff);
this.add.circle(400, 100, 64, 0x9966ff);
this.add.text(400, 100, 'Multi Texturing').setOrigin(0.5);

Эти объекты не используют загруженные извне текстуры. Прямоугольник и круг рендерятся с помощью шейдеров, генерирующих геометрию и цвет. Текст использует внутренний атлас шрифта Phaser. Каждый такой тип объекта образует свой собственный batch (пакет для отрисовки). Их наличие между группами спрайтов подчеркивает, что рендерер должен постоянно переключаться между разными типами рендеринга.

Что происходит под капотом WebGL-рендерера

Когда все объекты созданы, рендерер перед отрисовкой кадра выполняет сортировку. Алгоритм стремится сгруппировать вызовы отрисовки (draw calls) по принципу "одинаковая текстура + одинаковый шейдер".

В этом примере, несмотря на хаотичный порядок создания, все три спрайта 'bunny' должны быть отрисованы одним батчем, так же как и три 'brain', три 'block' и т.д. Примитивы и текст попадут в свои отдельные батчи.

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

Практические выводы для вашей игры

1. **Атласируйте текстуры.** Объединение множества мелких спрайтов в один большой атлас (спрайтшит) — лучший способ снизить количество переключений текстур до одного. 2. **Повторяйте текстуры.** Если объекты используют одну и ту же текстуру, рендерер сможет их сбатчить, даже если они далеко друг от друга на сцене. 3. **Избегайте частой смены текстур у динамических объектов.** Если спрайт в игровом процессе часто меняет setTexture, он будет выпадать из батчей, что может снизить производительность. 4. **Используйте конфигурацию.** Пример использует type: Phaser.WEBGL. Эта оптимизация недоступна при использовании Phaser.CANVAS.

const config = {
    type: Phaser.WEBGL, // Используйте WEBGL для батчинга
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

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

Мультитекстурирование и батчинг в Phaser — мощные инструменты оптимизации, работающие "из коробки" в WebGL-режиме. Понимая, как рендерер группирует вызовы отрисовки, вы сможете структурировать ресурсы игры для максимальной производительности. Для экспериментов попробуйте: создать сотни спрайтов со случайными текстурами из набора и сравнить FPS с аналогичным количеством спрайтов, использующих всего 2-3 текстуры; или визуализировать границы батчей, добавив кастомный отладочный рендеринг.