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

При разработке игр часто возникает задача отрисовки сложных композиций из множества спрайтов, которые должны вести себя как единый объект — например, вращаться вместе. Рендерить каждый спрайт по отдельности в таких случаях неэффективно. В этой статье мы разберем, как объединить группу игровых объектов в `Container`, а затем "запечь" его в `RenderTexture`, создав единую текстуру, которую можно анимировать с минимальными затратами производительности. Этот подход особенно полезен для создания сложных UI-элементов, статичных фоновых декораций или любых объектов, чья внутренняя структура не должна меняться каждый кадр, но которые нужно перемещать, вращать или масштабировать как одно целое.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    container;
    rt;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('lemming', 'assets/sprites/lemming.png');
    }

    create ()
    {
        this.container = this.add.container(400, 300).setVisible(false);

        //  Add some sprites - positions are relative to the Container x/y
        const sprite0 = this.add.sprite(0, 0, 'lemming');
        const sprite1 = this.add.sprite(-100, -100, 'lemming');
        const sprite2 = this.add.sprite(100, -100, 'lemming');
        const sprite3 = this.add.sprite(100, 100, 'lemming');
        const sprite4 = this.add.sprite(-100, 100, 'lemming');

        this.container.add([ sprite0, sprite1, sprite2, sprite3, sprite4 ]);

        this.rt = this.add.renderTexture(400, 300, 800, 600);

        this.rt.draw(this.container).render();
    }

    update ()
    {
        this.rt.camera.rotation -= 0.01;

        this.rt.clear()
        .draw(this.container)
        .render();
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и загрузка ассетов

В начале примера определяется класс сцены Example. В нем объявлены два ключевых свойства: container для группы объектов и rt для текстуры, в которую они будут отрисованы.

В методе preload загружается один спрайт — изображение лемминга. Обратите внимание, что для загрузки используется метод setBaseURL, который задает базовый путь для всех последующих загрузок. Это удобно, если все ваши ассеты лежат в одной директории.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('lemming', 'assets/sprites/lemming.png');
}

Создание и наполнение контейнера

В методе create происходит основная настройка. Сначала создается Container с помощью this.add.container(400, 300). Его координаты (400, 300) — это центр сцены при разрешении 800x600. Контейнер сразу же скрывается через setVisible(false), так как мы не хотим, чтобы он отрисовывался сам по себе. Его цель — лишь хранить и позиционировать свои дочерние элементы.

Затем создается пять спрайтов с ключом 'lemming'. Их координаты задаются относительно позиции контейнера. Например, спрайт с координатами (-100, -100) будет расположен слева вверху от центра контейнера.

const sprite0 = this.add.sprite(0, 0, 'lemming');
const sprite1 = this.add.sprite(-100, -100, 'lemming');
const sprite2 = this.add.sprite(100, -100, 'lemming');
const sprite3 = this.add.sprite(100, 100, 'lemming');
const sprite4 = this.add.sprite(-100, 100, 'lemming');

Все созданные спрайты добавляются в контейнер с помощью метода add, который принимает массив. Теперь эти спрайты являются дочерними элементами контейнера и будут наследовать его трансформации (позицию, вращение, масштаб).

Создание RenderTexture и первая отрисовка

Следующий шаг — создание RenderTexture. Это специальный игровой объект, который представляет собой динамическую текстуру, в которую можно отрисовывать другие объекты.

this.rt = this.add.renderTexture(400, 300, 800, 600);

Здесь создается текстура размером 800x600 с центром в точке (400, 300). После создания мы сразу вызываем цепочку методов:

this.rt.draw(this.container).render();

Метод draw добавляет контейнер в очередь отрисовки текстуры, а render выполняет саму отрисовку. В результате все спрайты из контейнера "запекаются" в единое изображение внутри rt. На экране мы видим не пять отдельных спрайтов, а одну текстуру с их изображениями.

Анимация и обновление текстуры

Логика анимации вынесена в метод update, который выполняется каждый кадр. Здесь происходит вращение камеры самой RenderTexture.

this.rt.camera.rotation -= 0.01;

Поскольку мы вращаем камеру текстуры, а не сами объекты, создается эффект вращения всего составного изображения.

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

this.rt.clear()
.draw(this.container)
.render();

Цепочка clear().draw().render() — это стандартный паттерн для обновления RenderTexture в реальном времени. Важно понимать, что контейнер (его внутренняя структура) не меняется, мы просто заново рисуем его в текстуру с учетом нового ракурса камеры.

Конфигурация и запуск игры

Пример завершается стандартной для Phaser 3 конфигурацией игры. В объекте config указывается тип рендерера, родительский DOM-элемент, размеры холста и класс основной сцены.

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

const game = new Phaser.Game(config);

Инициализация игры происходит через создание экземпляра Phaser.Game, которому передается конфигурационный объект. После этого Phaser берет на себя управление жизненным циклом сцены (вызовы preload, create, update).

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

Использование связки Container и RenderTexture позволяет оптимизировать отрисовку статичных групп объектов, превращая их в единую текстуру. Это снижает количество отрисовываемых объектов (draw calls) и повышает производительность, особенно на мобильных устройствах. Для экспериментов попробуйте: 1. Добавить анимацию не камере, а позициям спрайтов внутри контейнера перед вызовом draw. 2. Использовать RenderTexture для создания динамических мини-карт или порталов. 3. Применять фильтры или шейдеры к самой RenderTexture для получения сложных визуальных эффектов на всей группе объектов сразу.