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

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

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

Живой запуск

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

Исходный код


const USE_WEBGL = true; // Change to `true` to see bugs

class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('mario', 'assets/bugs/mario4x4.png');
        this.load.spritesheet('marioss', 'assets/bugs/mario4x4.png', { frameWidth: 128, frameHeight: 128 });
    }

    create ()
    {
        const origin = this.add.renderTexture(0, 0, 256, 256).setIsSpriteTexture(true);

        origin.draw('mario', 0, 0, 1, 0xffffff, true);

        origin.saveTexture('test-texture');

        const sqSize = 128;

        const f1 = origin.texture.add('square-1', 0, 0, 0, sqSize, sqSize);
        const f2 = origin.texture.add('square-2', 0, sqSize, 0, sqSize, sqSize);
        const f3 = origin.texture.add('square-3', 0, 0, sqSize, sqSize, sqSize);
        const f4 = origin.texture.add('square-4', 0, sqSize, sqSize, sqSize, sqSize);

        this.add.text(0, sqSize * 2, 'Original (RT)', {color: '#000', fontSize: 24}).setOrigin(0,0);

        // ----------- Test cases ------------------

        let offset = 256 + 10;
        // `Image`
        this.add.text(offset, sqSize * 2, 'Image', {color: '#000', fontSize: 24}).setOrigin(0,0);
        [
          ['square-1', 0, 0],
          ['square-2', sqSize+1, 0],
          ['square-3', 0, sqSize+1],
          ['square-4', sqSize+1, sqSize+1]
        ].forEach(([frame, x, y])=>{
          this.add.image(offset + x, y, 'test-texture', frame).setOrigin(0,0);
        });

        offset = (256 * 2) + (10 * 2);

        // `Sprite`
        this.add.text(offset, sqSize * 2, 'Sprite', {color: '#000', fontSize: 24}).setOrigin(0,0);
        [
          ['square-1', 0, 0],
          ['square-2', sqSize+1, 0],
          ['square-3', 0, sqSize+1],
          ['square-4', sqSize+1, sqSize+1]
        ].forEach(([frame, x, y])=>{
          this.add.sprite(offset + x, y, 'test-texture', frame).setOrigin(0,0);
        });

        offset = (256 * 3) + (10 * 3);

        // `Blitter`
        this.add.text(offset, sqSize * 2, 'Blitter', {color: '#000', fontSize: 24}).setOrigin(0,0);
        const blitter = this.add.blitter(0, 0, 'test-texture');
        [
          ['square-1', 0, 0],
          ['square-2', sqSize+1, 0],
          ['square-3', 0, sqSize+1],
          ['square-4', sqSize+1, sqSize+1]
        ].forEach(([frame, x, y])=>{
          blitter.create(offset + x, y, frame);
        });

        offset = (256 * 4) + (10 * 4);

        // `RenderTexture`
        this.add.text(offset, sqSize * 2, 'RT.drawFrame',  {color: '#000', fontSize: 24}).setOrigin(0,0);
        const target = this.add.renderTexture(offset, 0, 512, 512).setIsSpriteTexture(true);
        [
            ['square-1', 0, 0],
            ['square-2', sqSize+1, 0],
            ['square-3', 0, sqSize+1],
            ['square-4', sqSize+1, sqSize+1]
        ].forEach(([frame, x, y])=>{
          target.drawFrame('test-texture', frame, x, y);
        });
    }
}

new Phaser.Game({
  width: 1350,
  height: 524,
  type: USE_WEBGL ? Phaser.WEBGL : Phaser.CANVAS,
  backgroundColor: 0xFFFFFF,
  parent: 'phaser-example',
  scene: Example
});

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

В методе preload() загружаются два ресурса: обычное изображение и спрайтшит из одного и того же файла. Ключевой момент — использование одного файла mario4x4.png разными способами.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('mario', 'assets/bugs/mario4x4.png');
    this.load.spritesheet('marioss', 'assets/bugs/mario4x4.png', { frameWidth: 128, frameHeight: 128 });
}

Создание RenderTexture и сохранение текстуры

В create() создаётся объект RenderTexture размером 256x256 пикселей. Метод setIsSpriteTexture(true) позволяет использовать эту текстуру для создания спрайтов. Затем на эту текстуру рисуется исходное изображение 'mario' с помощью draw().

Самый важный шаг — вызов saveTexture('test-texture'). Этот метод регистрирует текущее содержимое RenderTexture в кэше текстур игры под ключом 'test-texture'. Теперь к этой текстуре можно обращаться по имени, как к любой загруженной.

const origin = this.add.renderTexture(0, 0, 256, 256).setIsSpriteTexture(true);
origin.draw('mario', 0, 0, 1, 0xffffff, true);
origin.saveTexture('test-texture');

Нарезка текстуры на кадры (фреймы)

После сохранения текстуры мы можем работать с её объектом Texture. Метод texture.add() добавляет в текстуру новые фреймы. Он принимает имя фрейма, индекс (0), координаты X, Y, ширину и высоту области.

В примере исходное изображение 256x256 нарезается на четыре квадрата 128x128 пикселей, каждому присваивается уникальное имя: 'square-1', 'square-2' и т.д.

const sqSize = 128;
const f1 = origin.texture.add('square-1', 0, 0, 0, sqSize, sqSize);
const f2 = origin.texture.add('square-2', 0, sqSize, 0, sqSize, sqSize);
const f3 = origin.texture.add('square-3', 0, 0, sqSize, sqSize, sqSize);
const f4 = origin.texture.add('square-4', 0, sqSize, sqSize, sqSize, sqSize);

Использование созданной текстуры в различных объектах

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

- `Image`: Статичное изображение. Создаётся через `this.add.image()`, где третьим аргументом передаётся имя текстуры ('test-texture'), а четвёртым — имя конкретного фрейма.
- `Sprite`: Анимируемый объект. Создаётся аналогично через `this.add.sprite()`.
- `Blitter`: Высокопроизводительный объект для отрисовки множества одинаковых спрайтов. Сначала создаётся сам `blitter`, затем для каждого фрейма вызывается `blitter.create()`.
- `RenderTexture` с `drawFrame()`: Позволяет отрисовать конкретный фрейм текстуры в другую `RenderTexture`. Используется метод `target.drawFrame('test-texture', frame, x, y)`.

Код для Image (остальные аналогичны по структуре):

this.add.text(offset, sqSize * 2, 'Image', {color: '#000', fontSize: 24}).setOrigin(0,0);
[
  ['square-1', 0, 0],
  ['square-2', sqSize+1, 0],
  ['square-3', 0, sqSize+1],
  ['square-4', sqSize+1, sqSize+1]
].forEach(([frame, x, y])=>{
  this.add.image(offset + x, y, 'test-texture', frame).setOrigin(0,0);
});

Конфигурация игры и переключение рендерера

В конце создаётся экземпляр игры Phaser.Game. Обратите внимание на переменную USE_WEBGL в самом начале файла. Она управляет типом рендерера: Phaser.WEBGL или Phaser.CANVAS. Этот пример был создан для демонстрации специфичных багов при использовании WebGL, поэтому переключение позволяет увидеть разницу в отображении.

new Phaser.Game({
  width: 1350,
  height: 524,
  type: USE_WEBGL ? Phaser.WEBGL : Phaser.CANVAS,
  backgroundColor: 0xFFFFFF,
  parent: 'phaser-example',
  scene: Example
});

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

Метод saveTexture() объекта RenderTexture открывает мощные возможности для runtime-генерации контента. Вы можете комбинировать изображения, рисовать фигуры, добавлять текст, а затем сохранять результат как новую текстуру для повторного использования. Для экспериментов попробуйте

  1. Нарисовать на RenderTexture несколько разных изображений перед сохранением
  2. Динамически создавать фреймы разного размера и формы
  3. Использовать полученную текстуру для создания частиц (ParticleEmitter)
  4. Менять параметры draw() (масштаб, tint) для получения различных визуальных эффектов