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

При создании игр с большим количеством объектов на экране, таких как стратегии или симуляторы, производительность может стать серьёзной проблемой. Классический подход с созданием сотен отдельных игровых объектов (Sprite) быстро приведёт к падению FPS. В этой статье мы разберём пример использования системы `Blitter` в Phaser — мощного инструмента для пакетного (batch) рендеринга множества одинаковых или похожих спрайтов. Вы научитесь создавать и эффективно управлять сеткой из 10 000 объектов без потери производительности.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });
    }

    create ()
    {
        const blitter = this.add.blitter(0, 0, 'bobs');

        //  Create a load of bobs aligned in a grid
        for (let y = 0; y < 100; y++)
        {
            for (let x = 0; x < 100; x++)
            {
                blitter.create(x * 32, y * 32, Phaser.Math.Between(0, 399));
            }
        }

        const cursors = this.input.keyboard.createCursorKeys();

        const controlConfig = {
            camera: this.cameras.main,
            left: cursors.left,
            right: cursors.right,
            up: cursors.up,
            down: cursors.down,
            zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
            zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
            acceleration: 0.06,
            drag: 0.0005,
            maxSpeed: 1.0
        };

        this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
    }

    update (time, delta)
    {
        this.controls.update(delta);
    }
}

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


const game = new Phaser.Game(config);

Что такое Blitter и зачем он нужен?

Blitter (от "bit block transfer") — это специальный игровой объект в Phaser, предназначенный для высокопроизводительного отображения множества спрайтов из одного и того же источника (текстуры или атласа). В отличие от обычного Sprite, который является самостоятельным объектом с физикой, анимацией и событиями, Blitter работает с "бобами" (bobs) — лёгкими инстансами, которые по сути являются инструкциями для отрисовки определённого кадра из текстуры в конкретной позиции.

Основная идея в том, что один Blitter управляет рендерингом тысяч таких инструкций за один draw call, что кардинально снижает нагрузку на GPU. Это идеально для статичных или редко меняющихся элементов: частиц, фоновых тайлов, звёзд на небе или, как в нашем примере, большой сетки спрайтов.

Разбор кода: Создание Blitter и заполнение сетки

В методе preload загружается спрайтшит — изображение, содержащее 400 отдельных кадров (frame) размером 32x32 пикселя.

this.load.spritesheet('bobs', 'assets/sprites/bobs-by-cleathley.png', { frameWidth: 32, frameHeight: 32 });

В методе create создаётся основной объект Blitter в точке (0,0), который будет использовать загруженный спрайтшит 'bobs'.

const blitter = this.add.blitter(0, 0, 'bobs');

Затем два вложенных цикла создают сетку 100x100, то есть 10 000 "бобов". Для каждого элемента вызывается метод blitter.create(x, y, frame). Ключевой момент — третий аргумент, определяющий, какой кадр из спрайтшита будет отрисован. В примере это случайное число от 0 до 399, что даёт разнообразную картинку.

for (let y = 0; y < 100; y++) {
    for (let x = 0; x < 100; x++) {
        blitter.create(x * 32, y * 32, Phaser.Math.Between(0, 399));
    }
}

Управление камерой с помощью SmoothedKeyControl

Поскольку мир теперь огромен (3200x3200 пикселей), нам необходим способ навигации. Phaser предоставляет для этого удобный класс Phaser.Cameras.Controls.SmoothedKeyControl.

Сначала создаётся объект конфигурации controlConfig. В нём указывается: * camera: Какая камера будет управляться (основная this.cameras.main). * Ключи для движения (стрелки) и зума (Q/E). * Параметры плавности: acceleration (ускорение), drag (сопротивление) и maxSpeed (максимальная скорость). Эти значения подобраны для плавного, "инерционного" движения.

const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
    zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
    acceleration: 0.06,
    drag: 0.0005,
    maxSpeed: 1.0
};

Затем создаётся сам контрол и сохраняется в свойство сцены. Его необходимо обновлять каждый кадр в методе update, передавая дельту времени для корректного расчёта движения.

this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
update (time, delta) {
    this.controls.update(delta);
}

Практические аспекты и производительность

Почему этот пример работает так быстро? Всё дело в архитектуре рендеринга WebGL. Blitter использует технологию batching — он собирает все данные о позициях и кадрах 10 000 "бобов" в один большой буфер и отправляет его на видеокарту за один раз. Если бы мы создали 10 000 объектов через this.add.sprite(), каждый из них был бы отдельным draw call с накладными расходами, что привело бы к катастрофическому падению производительности.

Важные ограничения и особенности Blitter: 1. **Нет встроенной физики или коллизий.** "Бобы" — это просто отрисовываемые изображения. 2. **Анимация возможна, но требует ручного управления.** Вы можете в update менять кадр у каждого "боба" через свойство frame. 3. **Идеальный use case:** статичный или малоподвижный фон, системы частиц (дождь, снег, звёзды), тайловые карты для больших миров, где каждый тайл — не отдельный игровой объект.

Для динамических объектов, которым нужны взаимодействия, всё ещё используйте Sprite или Image.

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

Blitter — это мощное и часто недооценённое оружие в арсенале разработчика игр на Phaser для задач, требующих отрисовки огромного количества визуально похожих элементов. Он позволяет легко преодолеть ограничения производительности, связанные с количеством draw calls. **Идеи для экспериментов:** 1. Замените случайный выбор кадра на вычисляемый (например, на основе шума Перлина), чтобы создать псевдослучайный ландшафт. 2. Реализуйте простую анимацию, перебирая кадры у всех или некоторых "бобов" в цикле update. 3. Добавьте интерактивности: используйте this.input.on('pointermove', ...) для изменения кадра "бобов" под курсором мыши, создавая эффект "растворения" или смены текстуры.