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

Работая с Phaser, вы наверняка сталкивались с необходимостью превратить динамичный игровой объект в текстуру для повторного использования или создания эффектов. Render Texture — это мощный инструмент для таких задач. В этой статье мы разберем пример захвата объекта с учетом его мировых координат и трансформаций, что позволит вам создавать сложные визуальные эффекты, такие как следы, голограммы или статические «фотографии» движущихся элементов, без избыточной нагрузки на производительность.

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

Живой запуск

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

Исходный код


// Capture a game object in the world to a render texture.
class Example extends Phaser.Scene
{
    shinyball;
    container;

    preload()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bush1', 'assets/sets/objects/bush1.png');
        this.load.image('bush2', 'assets/sets/objects/bush2.png');
        this.load.image('bush3', 'assets/sets/objects/bush3.png');
        this.load.image('bush4', 'assets/sets/objects/bush4.png');
        this.load.image('tree1', 'assets/sets/objects/tree1.png');
        this.load.image('tree2', 'assets/sets/objects/tree2.png');

        this.load.image('shinyball', 'assets/sprites/shinyball.png');
    }

    create ()
    {
        const width = this.renderer.width;
        const height = this.renderer.height;

        this.shinyball = this.add.image(0, 0, 'shinyball');
        this.container = this.add.container(width / 2, height * 0.85, [ this.shinyball ]);

        for (let i = 0; i < 16; i++)
        {
            this.add.image(
                Phaser.Math.Between(0, width), height,
                'tree' + Phaser.Math.Between(1, 2)
            )
                .setOrigin(0.5, 1)
                .setScale(0.5 + Math.random());
        }

        for (let i = 0; i < 32; i++)
        {
            this.add.image(Phaser.Math.Between(0, width), height, 'bush' + Phaser.Math.Between(1, 4)).setOrigin(0.5, 1);
        }

        const rt = this.add.renderTexture(width / 2, 0, width, height);
        rt
            .clear()
            .capture(this.shinyball, { transform: 'world' })
            .preserve(true)
            .setRenderMode('all');

    }

    update (time, delta)
    {
        // Update the container.
        this.container.x = Math.sin(time / 1000) * 400 + this.renderer.width / 2;
        this.container.y = Math.cos(time / 987) * 50 + this.renderer.height * 0.85;
        this.container.rotation = Math.sin(time / 876) * 0.5;

        // Update the shinyball.
        this.shinyball.x = Math.sin(time / 123) * 32;
        this.shinyball.y = Math.cos(time / 99) * 32;
        this.shinyball.rotation += 0.01;
        this.shinyball.setScale(1 + Math.sin(time / 567) * 0.1);
    }
}

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

const game = new Phaser.Game(config);

Зачем захватывать объект в Render Texture?

Класс Phaser.GameObjects.RenderTexture позволяет рисовать на текстуре, как на холсте. Метод capture() — ключевой инструмент для «фотографирования» других игровых объектов. Основная сложность в том, что объект может находиться внутри контейнера (Phaser.GameObjects.Container) и иметь сложную цепочку преобразований (позиция, масштаб, вращение).

Если захватывать объект с настройками по умолчанию, в текстуру попадет его локальное состояние относительно родителя. Но для эффектов, таких как след от движущегося шара, нужно учитывать его итоговое положение на сцене — мировые координаты. Именно это и делает параметр { transform: 'world' } в нашем примере.

Разбор сцены: создание окружения и целевого объекта

В методе create() сначала загружаются декорации (деревья и кусты) и спрайт «блестящего шара». Ключевые моменты:

- Шар (shinyball) создается в точке (0, 0), но сразу добавляется в контейнер (this.container). Это значит, его позиция теперь относительно позиции контейнера. - Контейнер размещается внизу сцены. Деревья и кусты добавляются случайным образом у нижнего края (height), с установленным setOrigin(0.5, 1) — это означает, что точка привязки (anchor) у них в центре снизу, поэтому они «стоят» на линии земли.

this.shinyball = this.add.image(0, 0, 'shinyball');
this.container = this.add.container(width / 2, height * 0.85, [ this.shinyball ]);
this.add.image(
    Phaser.Math.Between(0, width), height,
    'tree' + Phaser.Math.Between(1, 2)
)
    .setOrigin(0.5, 1)
    .setScale(0.5 + Math.random());

Магия захвата: Render Texture и параметр transform

Самое важное происходит после создания декораций. Мы создаем Render Texture размером во весь экран, центруем его по горизонтари и размещаем вверху (y=0).

const rt = this.add.renderTexture(width / 2, 0, width, height);
rt
    .clear()
    .capture(this.shinyball, { transform: 'world' })
    .preserve(true)
    .setRenderMode('all');

Разберем цепочку вызовов: 1. clear() — очищает текстуру от предыдущего содержимого. Важно делать это перед каждым новым захватом, если вы не хотите накопления. 2. capture(this.shinyball, { transform: 'world' }) — захватывает спрайт шара. Параметр transform: 'world' указывает движку использовать не локальную матрицу преобразований объекта относительно контейнера, а итоговую мировую матрицу. Благодаря этому в текстуру будет нарисован шар в том самом месте, где он виден на экране, со всеми наследованными от контейнера трансформациями. 3. preserve(true) — указывает, что содержимое Render Texture должно сохраняться между кадрами. Если бы мы установили false и вызывали capture в update(), текстура каждый раз очищалась бы и рисовалась заново. 4. setRenderMode('all') — устанавливает режим рендера. Это техническая деталь, гарантирующая корректное отображение всех элементов внутри текстуры.

Динамика: анимируем контейнер и объект

В методе update() реализовано движение, которое демонстрирует мощь захвата по мировым координатам.

- Контейнер движется по синусоиде по оси X и немного колеблется по оси Y, а также вращается. - Шар внутри контейнера совершает собственные независимые колебания, вращение и изменение масштаба.

// Update the container.
this.container.x = Math.sin(time / 1000) * 400 + this.renderer.width / 2;
this.container.y = Math.cos(time / 987) * 50 + this.renderer.height * 0.85;
this.container.rotation = Math.sin(time / 876) * 0.5;

// Update the shinyball.
this.shinyball.x = Math.sin(time / 123) * 32;
this.shinyball.y = Math.cos(time / 99) * 32;
this.shinyball.rotation += 0.01;
this.shinyball.setScale(1 + Math.sin(time / 567) * 0.1);

Несмотря на эту сложную иерархию и двойную анимацию, вызов capture в create() сделал лишь один снимок объекта в его начальном мировом положении. Чтобы текстура обновлялась в реальном времени, вам потребуется перенести вызов capture в метод update().

Практическое применение и вариации

Как использовать эту технику в реальном проекте?

- **Следы или шлейф:** Вызывайте rt.capture(object, { transform: 'world' }) каждый кадр в update() с preserve(true). Объект будет оставлять за собой "шлейф" из своих предыдущих положений. Постепенно уменьшайте альфа-канал текстуры (rt.setAlpha(rt.alpha * 0.99)) для эффекта затухания. - **Голограмма или призрак:** Захватите объект один раз, затем наложите полученную текстуру с эффектом свечения (setBlendMode(Phaser.BlendModes.ADD)) и анимируйте ее положение отдельно от оригинала. - **Создание спрайтов «на лету»:** Захватите сложную композицию объектов (например, персонажа с оружием) в одну текстуру. Затем вы можете использовать эту текстуру как изображение для нового спрайта, что может быть полезно для оптимизации.

Важно: захват в текстуру — относительно ресурсоемкая операция. Старайтесь не делать это для множества объектов каждый кадр.

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

Метод capture объекта RenderTexture с параметром { transform: 'world' } — это ключ к мощным визуальным эффектам в Phaser, которые учитывают иерархию и трансформации объектов. Начните экспериментировать: попробуйте захватывать не один объект, а группу, поиграйте с режимами наложения (blendMode) самой текстуры или создайте эффект "заморозки времени", периодически сохраняя в текстуру состояние целой области игрового мира.