О чем этот пример
Работая с 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) самой текстуры или создайте эффект "заморозки времени", периодически сохраняя в текстуру состояние целой области игрового мира.
