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

Разработка игр часто упирается в производительность: как отобразить сотни анимированных объектов без падения FPS? Встроенный в Phaser объект `Blitter` — это секретное оружие для работы с большим количеством спрайтов. Он использует технологию batching, чтобы отрисовывать тысячи элементов за один вызов WebGL. В этой статье мы разберем практический пример, где `Blitter` комбинируется с системой твинов для создания плавной анимации частиц, и научимся отслеживать производительность в реальном времени.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    numbers = [];
    frame = 'veg01';
    idx = 1;
    blitter;

    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');
    }

    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.blitter = this.add.blitter(0, 0, 'atlas');

        for (let i = 0; i < 32; i++)
        {
            this.launch(i);
        }
        
        this.updateDigits();
    }

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

            this.updateDigits();
        }
    }

    launch (i)
    {
        this.idx++;

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

        if (this.idx < 10)
        {
            this.frame = `veg0${this.idx.toString()}`;
        }
        else
        {
            this.frame = `veg${this.idx.toString()}`;
        }

        const bob = this.blitter.create(i * 32, 0, this.frame);

        this.tweens.add({
            targets: bob,
            duration: 2000,
            y: 650,
            delay: Math.random() * 2,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true
        });
    }

    updateDigits ()
    {
        const len = Phaser.Utils.String.Pad(this.blitter.children.list.length.toString(), 6, '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]);
    }
}

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

const game = new Phaser.Game(config);

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

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

Это критически важно для производительности, особенно при работе с частицами, каплями дождя, звёздами на небе или, как в нашем примере, с падающими фруктами. Каждый элемент (bob) внутри Blitter — это просто ссылка на область (frame) в текстуре атласа и координаты для отрисовки.

Создание Blitter в сцене выглядит так:

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

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

Анатомия примера: запуск и анимация частиц

В методе create() создаётся 32 начальных частицы, а в update() — по 32 новых на каждый клик мыши. Ключевой метод launch() отвечает за создание и анимацию каждой частицы.

Сначала он вычисляет имя кадра (frame) из атласа. Атлас в примере содержит кадры с именами от veg01 до veg37. Код циклически перебирает их.

if (this.idx < 10) {
    this.frame = `veg0${this.idx.toString()}`;
} else {
    this.frame = `veg${this.idx.toString()}`;
}

Затем создаётся сам bob — визуальный элемент внутри Blitter.

const bob = this.blitter.create(i * 32, 0, this.frame);

Параметры: начальная координата X, Y и имя кадра из атласа. После создания к bob сразу применяется твин — плавная анимация движения по оси Y.

this.tweens.add({
    targets: bob,
    duration: 2000,
    y: 650,
    delay: Math.random() * 2,
    ease: 'Sine.easeInOut',
    repeat: -1,
    yoyo: true
});

Здесь targets: bob указывает, что анимация применяется к конкретному bob. Параметры repeat: -1 и yoyo: true заставляют анимацию бесконечно повторяться в прямом и обратном направлении, создавая эффект «подпрыгивания». Math.random() в delay добавляет разброс во времени запуска, чтобы частицы двигались не синхронно.

Считываем метрики: счётчик объектов в реальном времени

Визуальный счётчик внизу экрана — не просто украшение. Это инструмент для отслеживания количества активных bob-объектов внутри Blitter. Он демонстрирует, как легко можно создать систему мониторинга прямо в игре.

В create() создаётся массив из шести объектов Image, которые будут отображать цифры.

this.numbers.push(this.add.image(32 + 0 * 50, 742, 'atlas', '0'));
// ... и так еще 5 раз

Каждое изображение использует тот же атлас 'atlas', где цифры от '0' до '9' также являются отдельными кадрами.

Метод updateDigits() обновляет эти цифры.

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

this.numbers[0].setFrame(len[0]);
this.numbers[1].setFrame(len[1]);
// ... и так для всех шести цифр

this.blitter.children.list — это массив всех активных bob-объектов. Его длина преобразуется в строку и дополняется нулями слева до 6 символов с помощью Phaser.Utils.String.Pad. Затем каждый объект Image в массиве numbers получает свой кадр-цифру через метод setFrame(). Таким образом, мы видим точное количество частиц, например, «000032» или «000512».

Оптимизация и практические советы

1. **Атлас текстуры обязателен.** Blitter работает только с одним источником текстур. Все кадры для bob должны находиться в одном атласе, загруженном в preload(). 2. **Твины — это безопасно.** Система твинов Phaser (this.tweens.add) оптимизирована для работы с большим количеством целей. Анимировать свойства bob (x, y, alpha) с её помощью — стандартная и эффективная практика. 3. **Управление памятью.** В примере bob создаются, но не уничтожаются. В реальном проекте для частиц, которые исчезают, следует использовать пулы объектов или удалять bob по завершении анимации, чтобы избежать утечки памяти. 4. **Используйте children.list.** Это прямой доступ к массиву всех bob для подсчёта, итерации или массовых операций.

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

Комбинация Blitter для эффективного рендеринга и системы твинов для плавной анимации — мощный паттерн в Phaser для создания сложных визуальных эффектов с тысячами объектов. Этот подход лежит в основе систем частиц, фоновых декораций и массовых сцен. **Идеи для экспериментов:** 1. Измените твин: добавьте анимацию прозрачности (alpha) или масштаба (scaleX, scaleY). 2. Реализуйте удаление bob через 5 секунд после создания, используя setTimeout или событие твина onComplete. 3. Замените реакцию на клик на автоматический спавн частиц с интервалом (setInterval) и посмотрите, при каком их количестве начнутся заметные просадки FPS. 4. Создайте несколько Blitter для разных текстур и управляйте их глубиной (depth), чтобы формировать слои заднего фона.