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

Создание сотен или тысяч плавных анимаций (tweens) — частый запрос в играх: падающие листья, частицы, фоновые объекты. Но как Phaser справляется с такой нагрузкой, и как эффективно управлять массовыми анимациями, не теряя в производительности? Этот пример — наглядный стресс-тест для системы твинов движка. Он демонстрирует, как с помощью `Blitter` и одного обработчика событий можно мгновенно создавать сотни анимированных спрайтов. Мы разберем код, чтобы понять принципы эффективной работы с анимациями в больших количествах.

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

Живой запуск

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

Исходный код


var scene = null;
var add = false;
var blitter;
var idx = 1;
var frame = 'veg01';
var numbers = [];

var text;
var tween;

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

    launch(i)
    {
        idx++;

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

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

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

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

    create()
    {
        scene = this;

        numbers.push(this.add.image(0 * 48, 720, 'atlas', '0').setOrigin(0));
        numbers.push(this.add.image(1 * 48, 720, 'atlas', '0').setOrigin(0));
        numbers.push(this.add.image(2 * 48, 720, 'atlas', '0').setOrigin(0));
        numbers.push(this.add.image(3 * 48, 720, 'atlas', '0').setOrigin(0));
        numbers.push(this.add.image(4 * 48, 720, 'atlas', '0').setOrigin(0));
        numbers.push(this.add.image(5 * 48, 720, 'atlas', '0').setOrigin(0));

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

        this.updateDigits();

        this.input.on('pointerdown', function ()
        {

            add = true;

        });

        this.input.on('pointerup', function ()
        {

            add = false;

        });
    }

    update()
    {
        if (add)
        {
            for (var i = 0; i < 256; ++i)
            {
                this.launch(i);
            }

            this.updateDigits();
        }
    }

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

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

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

const game = new Phaser.Game(config);

Архитектура примера: Blitter как фабрика спрайтов

Вместо создания сотен отдельных игровых объектов (Image или Sprite) пример использует Blitter. Это специальный объект Phaser для высокопроизводительного отображения множества однотипных спрайтов из одного атласа.

Инициализация происходит в методе create(). Создается один Blitter, который будет хранить и отрисовывать все наши анимированные овощи и фрукты.

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

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

numbers.push(this.add.image(0 * 48, 720, 'atlas', '0').setOrigin(0));
// ... и так для всех шести позиций

Использование Blitter вместо отдельных Sprite — ключевое решение для производительности, так как он оптимизирован для группового рендеринга.

Механика запуска: создание спрайта и его твин

Сердце примера — метод launch(i). Он вызывается в цикле и отвечает за создание одного анимированного элемента.

Сначала метод обновляет глобальную переменную frame, перебирая кадры атласа от 'veg01' до 'veg37'. Это создает визуальное разнообразие.

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

Затем внутри Blitter создается новый Bob (представление спрайта в Blitter) с заданными координатами и кадром.

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

Сразу после создания к этому bob применяется твин. Настройки твина заставляют спрайт двигаться по вертикали от верхней точки до y=650 и обратно, с задержкой и плавным easing.

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

Обратите внимание: targets — это сам bob, а не blitter. Каждый bob получает свой независимый твин. Параметр repeat: -1 делает анимацию бесконечной, а yoyo: true означает, что она будет проигрываться в прямом и обратном порядке.

Управление потоком: как добавляются сотни объектов

Пользователь управляет процессом через мышь. При зажатой кнопке (pointerdown) флаг add устанавливается в true. В методе update(), который вызывается на каждом кадре, проверяется этот флаг.

Если add = true, запускается цикл на 256 итераций, и на каждой итерации вызывается this.launch(i). Таким образом, пока кнопка зажата, каждый кадр добавляет 256 новых анимированных спрайтов. Это создает лавинообразный рост количества объектов и нагрузки на систему твинов.

update() {
    if (add) {
        for (var i = 0; i < 256; ++i) {
            this.launch(i);
        }
        this.updateDigits();
    }
}

После каждого добавления блока из 256 спрайтов вызывается метод updateDigits(), который обновляет цифровое табло внизу экрана.

Визуализация счетчика: работа с утилитами Phaser

Метод updateDigits() показывает, как можно работать со вспомогательными утилитами Phaser. Его задача — взять текущее количество детей в blitter, преобразовать его в шестизначную строку и отобразить каждую цифру как отдельный кадр в спрайтах из массива numbers.

Для форматирования числа используется Phaser.Utils.String.Pad. Она дополняет строку нулями слева до нужной длины (6 символов).

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

Затем в цикле (в коде он развернут) для каждого спрайта-цифры вызывается метод setFrame(), куда передается соответствующий символ из строки len. Поскольку имена кадров для цифр в атласе совпадают с символами ('0', '1' и т.д.), это работает.

numbers[0].setFrame(len[0]);
numbers[1].setFrame(len[1]);
// ...

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

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

Пример наглядно демонстрирует производительность системы твинов Phaser 3 и правильный паттерн для массового создания анимаций через Blitter. Даже при десятках тысяч активных твинов (если удерживать кнопку) движок сохраняет плавность, что говорит об эффективной внутренней оптимизации. **Идеи для экспериментов:** 1. Измените параметры твина в launch(): попробуйте другие easing-функции (Power2, Bounce), измените длительность, добавьте анимацию по оси X. 2. Вместо случайной задержки (delay) задайте зависимость от индекса `i`, чтобы создать "волновой" эффект. 3. Ограничьте общее количество создаваемых спрайтов и реализуйте их повторное использование (пул объектов) для еще большей оптимизации. 4. Замените Blitter на группу (this.add.group()) со спрайтами и сравните производительность в инструментах разработчика.