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

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

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

Живой запуск

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

Исходный код


let blitter;
let gravity = 0.5;
let idx = 1;

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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('atlas', 'assets/tests/fruit/veg.png', 'assets/tests/fruit/veg.json');
        this.numbers = [];
        this.iter = 0;
    }

    launch () {
        let frame = 'veg01';
        idx++;

        if (idx === 38)
        {
            idx = 1;
        }

        if (idx < 10)
        {
            frame = 'veg0' + idx.toString();
        }
        else
        {
            frame = 'veg' + idx.toString();
        }

        const bob = blitter.create(0, 0, frame);

        bob.data.vx = Math.random() * 10;
        bob.data.vy = Math.random() * 10;
        bob.data.bounce = 0.8 + (Math.random() * 0.3);
    }

    create ()
    {
        this.numbers.push(this.add.image(32 + 0 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 1 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 2 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 3 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 4 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 5 * 50, 742, 'atlas', '0'));
        this.numbers.push(this.add.image(32 + 6 * 50, 742, 'atlas', '0'));

        blitter = this.add.blitter(0, 0, 'atlas');

        for (var i = 0; i < 100; ++i)
        {
            this.launch();
        }

        this.updateDigits();
    }

    update ()
    {
        if (this.input.activePointer.isDown)
        {
            for (var i = 0; i < 250; ++i)
            {
                this.launch();
            }

            this.updateDigits();
        }

        for (var index = 0, length = blitter.children.list.length; index < length; ++index)
        {
            var bob = blitter.children.list[index];

            bob.data.vy += gravity;

            bob.y += bob.data.vy;
            bob.x += bob.data.vx;

            if (bob.x > 1024)
            {
                bob.x = 1024;
                bob.data.vx *= -bob.data.bounce;
            }
            else if (bob.x < 0)
            {
                bob.x = 0;
                bob.data.vx *= -bob.data.bounce;
            }

            if (bob.y > 684)
            {
                bob.y = 684;
                bob.data.vy *= -bob.data.bounce;
            }
        }

        // this.cameras.main.scrollX = Math.sin(this.iter) * 200;
        // this.iter += 0.01;
    }

    updateDigits ()
    {
        const len = Phaser.Utils.String.Pad(blitter.children.list.length.toString(), 7, '0', 1);

        this.numbers[0].setFrame(len[0]);
        this.numbers[1].setFrame(len[1]);
        this.numbers[2].setFrame(len[2]);
        this.numbers[3].setFrame(len[3]);
        this.numbers[4].setFrame(len[4]);
        this.numbers[5].setFrame(len[5]);
        this.numbers[6].setFrame(len[6]);
    }

}

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

const game = new Phaser.Game(config);

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

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

Каждый Bob — это, по сути, инструкция для отрисовки: координаты (x, y) и ссылка на кадр в текстуре. Это делает операцию отрисовки тысячи объектов невероятно эффективной для GPU, так как минимизирует количество вызовов отрисовки (draw calls) и переключений состояний.

blitter = this.add.blitter(0, 0, 'atlas');

В этом примере мы создаём Blitter, который будет использовать загруженный атлас 'atlas'.

Инициализация сцены и создание объектов Bob

В методе create() сцена инициализируется. Сначала создаются семь изображений (this.add.image) для отображения счётчика объектов. Они не относятся к Blitter и служат для визуализации количества созданных Bob.

Затем создаётся сам Blitter. После этого в цикле 100 раз вызывается метод launch(), который создаёт начальную партию из 100 падающих объектов.

for (var i = 0; i < 100; ++i) {
    this.launch();
}

Метод launch() — это сердце логики создания объектов. Он: 1. Определяет, какой кадр (frame) из атласа использовать, перебирая их по порядку. 2. Создаёт новый объект Bob в позиции (0, 0) через метод blitter.create(). 3. Записывает в свойство data каждого Bob его уникальные физические параметры: начальные скорости vx, vy и коэффициент упругости bounce.

const bob = blitter.create(0, 0, frame);
bob.data.vx = Math.random() * 10;
bob.data.vy = Math.random() * 10;
bob.data.bounce = 0.8 + (Math.random() * 0.3);

Важно: bob.data — это пользовательское свойство, которое Phaser не обрабатывает автоматически. Мы используем его как хранилище для нашей кастомной логики.

Кастомная физика в реальном времени

Вся физика движения реализована вручную в методе update(). Это демонстрирует гибкость подхода: Blitter отвечает только за отрисовку, а логику поведения вы программируете сами, итеративно обрабатывая массив blitter.children.list.

На каждом кадре для каждого Bob: 1. К вертикальной скорости добавляется гравитация (gravity). 2. Позиция обновляется на основе скорости. 3. Проверяется столкновение с границами экрана (0, 1024 по X и 684 по Y). При столкновении скорость инвертируется и умножается на коэффициент bounce, имитируя упругость.

for (var index = 0, length = blitter.children.list.length; index < length; ++index) {
    var bob = blitter.children.list[index];
    bob.data.vy += gravity;
    bob.y += bob.data.vy;
    bob.x += bob.data.vx;
    if (bob.x > 1024) {
        bob.x = 1024;
        bob.data.vx *= -bob.data.bounce;
    }
    // ... обработка других границ
}

Также в update() есть блок, который при зажатой кнопке мыши (this.input.activePointer.isDown) мгновенно создаёт 250 новых объектов, показывая, насколько быстра операция создания Bob.

Визуализация счётчика и полезные утилиты

В примере есть отличный приём для отображения числовых значений с помощью того же графического атласа. Метод updateDigits() обновляет кадры у семи заранее созданных изображений, чтобы показать текущее количество Bob.

Здесь используется утилита Phaser.Utils.String.Pad для форматирования числа: она гарантирует, что в строке всегда будет 7 символов, дополняя её нулями слева при необходимости. Это нужно, чтобы обращаться к конкретным цифрам строки по индексу.

const len = Phaser.Utils.String.Pad(blitter.children.list.length.toString(), 7, '0', 1);
this.numbers[0].setFrame(len[0]); // Устанавливаем первый символ строки как кадр
this.numbers[1].setFrame(len[1]);
// ... и так далее

Такой подход позволяет использовать один атлас и для игровых объектов, и для UI-элементов.

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

Blitter в Phaser — это мощный инструмент для оптимизации, когда требуется высокая плотность однотипных объектов. Он берёт на себя эффективную отрисовку, оставляя вам полный контроль над логикой каждого элемента через простой цикл обхода children.list. Для экспериментов попробуйте: увеличить гравитацию или начальную скорость; изменить логику создания объектов по времени, а не по клику; добавить ветер (постоянное смещение по X); или, раскомментировав строки в update(), заставить камеру плавно колебаться, чтобы убедиться, что производительность остаётся стабильной даже при движении видимой области.