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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    blitter;
    tilesprite;
    quad;
    graphics;
    bitmaptext;
    particles;
    bunny;
    text;
    bob;
    rt;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bunny', 'assets/sprites/bunny.png');
        this.load.image('pic', 'assets/pics/baal-loader.png');
        this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
        this.load.bitmapFont('desyrel', 'assets/fonts/bitmap/desyrel.png', 'assets/fonts/bitmap/desyrel.xml');
        this.load.image('image', 'assets/pics/sure-shot-by-made.png');
        this.load.image('mushroom', 'assets/sprites/mushroom2.png');
        this.load.image('atari', 'assets/sprites/atari130xe.png');
    }

    create ()
    {
        this.bunny = this.textures.getFrame('bunny');
        this.text = this.add.text(0, 0, 'phaser 3?').setVisible(false);
        this.bob = this.add.image(0, 0, 'bunny').setName('bob').setVisible(false);

        this.particles = this.add.particles(200, 300, 'flares', {
            frame: 'blue',
            lifespan: 2000,
            speed: { min: 400, max: 600 },
            angle: 330,
            gravityY: 300,
            scale: { start: 0.4, end: 0 },
            quantity: 2,
            blendMode: 'ADD',
            advance: 1000
        }).setVisible(false);

        this.bitmaptext = this.add.bitmapText(0, 0, 'desyrel', 'PHASER 3\nRender Texture').setVisible(false);

        this.graphics = this.add.graphics().setVisible(false);

        this.graphics.fillStyle(0xffff00, 1);

        this.graphics.slice(400, 300, 200, Phaser.Math.DegToRad(340), Phaser.Math.DegToRad(20), true);

        this.graphics.fillPath();

        this.tilesprite = this.add.tileSprite(400, 300, 250, 250, 'mushroom').setVisible(false);

        this.blitter = this.add.blitter(0, 0, 'atari').setVisible(false);

        this.blitter.create(0, 0);

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

        this.rt.draw(this.graphics, 0, 0);
        this.rt.draw(this.bob, 200, 200);
        this.rt.draw(this.tilesprite, 200, 200);
        this.rt.draw(this.blitter, 0, 0);
        this.rt.draw(this.text, 100, 100);
        this.rt.draw(this.bob, 300, 300);
        this.rt.draw(this.bob, 400, 400);
        this.rt.draw(this.text, 300, 200);
        this.rt.draw(this.particles, 300, 100);
        this.rt.draw(this.bitmaptext, 200, 100);

        this.rt.render();

        this.input.on('pointerdown', () =>
        {
            this.rt.resize(400, 300);

            this.rt.draw(this.graphics, 0, 0);
            this.rt.draw(this.bob, 200, 200);
            this.rt.draw(this.tilesprite, 200, 200);
            this.rt.draw(this.blitter, 0, 0);
            this.rt.draw(this.text, 100, 100);
            this.rt.draw(this.bob, 300, 300);
            this.rt.draw(this.bob, 400, 400);
            this.rt.draw(this.text, 300, 200);
            this.rt.draw(this.particles, 300, 100);
            this.rt.draw(this.bitmaptext, 200, 100);

            this.rt.render();

        });
    }
}

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

const game = new Phaser.Game(config);

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

Объект RenderTexture (или RenderTexture в более новых версиях Phaser) представляет собой особый тип текстуры, которая действует как буфер для рендеринга. Вы можете «рисовать» на ней другие игровые объекты, используя метод draw(). Результат сохраняется в текстуре, которую затем можно использовать как изображение для спрайта или отображать самостоятельно.

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

Подготовка и создание объектов для отрисовки

Перед созданием RenderTexture необходимо подготовить объекты, которые будут на неё нарисованы. В методе create() сцены создаются различные типы объектов Phaser, но с важным нюансом: они изначально невидимы (setVisible(false)). Это нужно потому, что мы не хотим, чтобы они отображались на основном экране сами по себе — их конечная цель быть отрендеренными внутрь текстуры.

Создаются следующие объекты:
- Текстовый объект (`this.add.text`).
- Изображение (`this.add.image`).
- Система частиц (`this.add.particles`).
- Bitmap-текст (`this.add.bitmapText`).
- Графический объект (`this.add.graphics`) с нарисованной фигурой.
- TileSprite.
- Blitter-объект.
this.bob = this.add.image(0, 0, 'bunny').setName('bob').setVisible(false);
this.graphics = this.add.graphics().setVisible(false);
this.bitmaptext = this.add.bitmapText(0, 0, 'desyrel', 'PHASER 3\nRender Texture').setVisible(false);

Создание Render Texture и первоначальная отрисовка

После подготовки объектов создается сама RenderTexture. Её конструктор принимает координаты (x, y), ширину и высоту. В данном случае текстура создается размером 800x600 пикселей в центре экрана (координаты 400, 300).

Затем, используя метод draw(), мы помещаем в текстуру все подготовленные объекты. Важно понимать, что координаты внутри метода draw() отсчитываются от верхнего левого угла самой RenderTexture, а не от глобальных координат сцены. Это позволяет точно позиционировать объекты внутри текстуры.

Метод render() вызывается для того, чтобы все нарисованные команды были применены и текстура стала видимой на экране.

this.rt = this.add.renderTexture(400, 300, 800, 600);
this.rt.draw(this.graphics, 0, 0);
this.rt.draw(this.bob, 200, 200);
// ... другие вызовы draw
this.rt.render();

Динамическое изменение размера текстуры

Самая интересная часть примера — это изменение размера RenderTexture «на лету». По клику мыши (событие pointerdown) вызывается метод resize() у объекта this.rt. Этот метод принимает новую ширину и высоту. В примере размер уменьшается с 800x600 до 400x300 пикселей.

**Важный момент:** После изменения размера текстура очищается. Все ранее нарисованные объекты пропадают. Поэтому сразу после вызова resize() необходимо заново выполнить всю последовательность вызовов draw() для заполнения текстуры актуальным содержимым, а затем снова вызвать render(). Это поведение отличает resize() от простого масштабирования — происходит пересоздание внутреннего буфера текстуры.

this.input.on('pointerdown', () => {
    this.rt.resize(400, 300); // Изменяем размер буфера
    // Повторяем все команды рисования для нового размера
    this.rt.draw(this.graphics, 0, 0);
    this.rt.draw(this.bob, 200, 200);
    // ...
    this.rt.render(); // Применяем изменения
});

Практические применения и особенности

Динамический resize() может быть полезен в нескольких сценариях: 1. **Создание адаптивных элементов интерфейса**, размер которых зависит от действий игрока или размера окна. 2. **Эффекты перехода или трансформации**, где текстура должна плавно сжиматься или растягиваться. 3. **Оптимизация:** можно уменьшить размер текстуры для рендеринга менее детализированных версий сложных композиций (например, для мини-карты в углу экрана).

**На что обратить внимание:** - После resize() требуется полная перерисовка содержимого. - Координаты для draw() после ресайза должны быть пересчитаны, если нужна относительная привязка. В примере используются абсолютные значения, поэтому при уменьшении текстуры в 2 раза объекты будут нарисованы в тех же координатах относительно нового, меньшего холста, что визуально «приблизит» и сместит композицию. - Изменение размера — операция, требующая пересоздания внутреннего WebGL-текстура, поэтому частые вызовы resize() могут сказаться на производительности.

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

Render Texture в Phaser 3 — это гибкий инструмент для композитинга, а возможность динамического изменения её размера методом resize() добавляет новый уровень контроля. Помните, что после изменения размера текстура очищается, и её содержимое нужно перерисовать. **Идеи для экспериментов:** 1. Свяжите размер RenderTexture с движением мыши или жестами на сенсорном экране. 2. Реализуйте плавную анимацию изменения размера с помощью Tween, вызывая resize() на каждом кадре с новыми значениями. 3. Используйте текстуру с измененным размером в качестве маски для другого объекта. 4. Попробуйте не перерисовывать все объекты после resize(), а сохранить оригинальную текстуру в другой RenderTexture и нарисовать её с масштабированием в новую, меньшую текстуру, используя параметры draw() для масштаба.