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

В процессе разработки игр часто возникает необходимость сделать скриншот текущего состояния canvas — например, для создания системы реплеев, сохранения прогресса или генерации превью. Phaser предоставляет для этого два метода: `renderer.snapshot` и `renderTexture.snapshot`. В этой статье мы разберемся, как они работают и в чем ключевое отличие между ними, используя реальный пример из баг-трекера. Понимание разницы между этими методами позволит вам избежать распространенной ошибки, когда снимок `RenderTexture` оказывается пустым, и выбирать правильный инструмент для задачи.

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

Живой запуск

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

Исходный код


class Test extends Phaser.Scene {

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('image0', 'assets/pics/ra-einstein.png');
        this.load.image('image1', 'assets/sprites/mushroom2.png');
    }

    create ()
    {
        this.add.tileSprite(400, 300, 800, 600, 'image1');

        const text = this.add.text(10, 10, "Hello!", {
			fontSize: "48px"
		});

		const renderTextrure = this.add.renderTexture(10, 100, 300, 300);

        renderTextrure.draw(text);

        this.input.once('pointerdown', () => {

            this.sys.renderer.snapshot(img => {
                img.style.position = "fixed";
                img.style.left = window.innerWidth / 2 - 350 + "px";
                img.style.top = window.innerHeight / 2 - 150 + "px";
                img.style.border = "solid 1px red";
                document.body.appendChild(img);
            });

            /*
            renderTextrure.snapshot(img => {
                img.style.position = "fixed";
                img.style.left = window.innerWidth / 2 - 350 + "px";
                img.style.top = window.innerHeight / 2 - 150 + "px";
                img.style.border = "solid 1px red";
                document.body.appendChild(img);
            });
            */

        });
	}
}

var game = new Phaser.Game({
    width: 800,
    height: 600,
    type: Phaser.AUTO,
    parent: 'phaser-example',
    backgroundColor: "#242424",
    scene: Test
});

Подготовка сцены и создание объектов

В начале примера мы видим стандартную структуру сцены Phaser с методами preload и create. В preload загружаются два изображения, которые будут использоваться в сцене.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('image0', 'assets/pics/ra-einstein.png');
this.load.image('image1', 'assets/sprites/mushroom2.png');

В методе create происходит инициализация графических объектов. Сначала создается TileSprite, который заполняет фон текстурой гриба ('image1'). Затем добавляется текстовый объект с надписью "Hello!".

this.add.tileSprite(400, 300, 800, 600, 'image1');
const text = this.add.text(10, 10, "Hello!", {
	fontSize: "48px"
});

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

const renderTextrure = this.add.renderTexture(10, 100, 300, 300);
renderTextrure.draw(text);

На этом этапе в левом верхнем углу игрового поля мы видим текст "Hello!", отрисованный как обычный объект сцены, и копию этого текста внутри RenderTexture, расположенную чуть ниже.

Захват снимка всего рендерера (renderer.snapshot)

Основная логика примера активируется по клику мыши. Обработчик события pointerdown выполняет один раз (once) функцию, которая создает снимок.

Первый и рабочий способ — использовать метод snapshot у объекта рендерера игры, доступного через this.sys.renderer.

this.sys.renderer.snapshot(img => {
    img.style.position = "fixed";
    img.style.left = window.innerWidth / 2 - 350 + "px";
    img.style.top = window.innerHeight / 2 - 150 + "px";
    img.style.border = "solid 1px red";
    document.body.appendChild(img);
});

Метод this.sys.renderer.snapshot принимает колбэк, в который передается готовый HTML-элемент <img>. Этот элемент содержит снимок *всего* игрового canvas на момент вызова. В коде примера этому изображению задается CSS-позиционирование, чтобы вывести его поверх страницы, и добавляется красная рамка для наглядности. В результате мы увидим картинку, на которой запечатлены и фон-тайл, и исходный текст "Hello!", и RenderTexture с его копией.

Проблема с захватом RenderTexture (renderTexture.snapshot)

В примере закомментирован второй блок кода, который как раз и демонстрирует проблему (bug). Вместо снимка всего рендерера здесь предпринимается попытка сделать снимок конкретного объекта RenderTexture.

/*
renderTextrure.snapshot(img => {
    img.style.position = "fixed";
    img.style.left = window.innerWidth / 2 - 350 + "px";
    img.style.top = window.innerHeight / 2 - 150 + "px";
    img.style.border = "solid 1px red";
    document.body.appendChild(img);
});
*/

Если раскомментировать этот код, результат, скорее всего, окажется неожиданным: созданное изображение будет либо полностью пустым (прозрачным), либо содержать артефакты. Это происходит потому, что RenderTexture в Phaser 3 — это не самостоятельный canvas, а текстура в памяти WebGL (или Canvas) контекста. Метод snapshot для RenderTexture может работать некорректно или требовать особых условий для рендеринга (например, завершения всех отложенных операций с графикой), которые в данном простом примере не выполняются. Этот код и был помещен в баг-трекер для исправления.

Практические выводы и обходные пути

Итак, главный вывод: для создания надежного скриншота в Phaser 3 используйте this.sys.renderer.snapshot() или this.renderer.snapshot(). Этот метод делает снимок итогового кадра, отображенного на экране.

Если же ваша задача — получить изображение содержимого конкретного RenderTexture (например, сгенерированного спрайта или части UI), нужно действовать иначе. Один из рабочих подходов: 1. Создать временную сцену или камеру. 2. Отобразить нужный RenderTexture как единственный объект в этой сцене или в поле зрения камеры. 3. Сделать snapshot рендерера в этот момент.

// Примерный псевдокод обходного пути
this.tempScene = this.scene.add('temp', {
    create: function() {
        this.add.image(0, 0, 'renderTextureKey');
        this.sys.renderer.snapshot(callback);
        this.scene.remove(); // Удаляем временную сцену
    }
});
this.scene.launch('temp');

Всегда проверяйте актуальность API в документации Phaser, так как поведение renderTexture.snapshot может быть изменено в будущих версиях.

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

Метод renderer.snapshot — это надежный инструмент для создания снимков всего игрового поля. Метод renderTexture.snapshot, представленный в примере, в текущей реализации может не работать как ожидается, и его следует использовать с осторожностью или искать альтернативы. Для экспериментов попробуйте

  1. Сравнить снимки, сделанные в WebGL и Canvas-режимах рендерера
  2. Реализовать систему автоматических скриншотов при достижении игроком нового уровня
  3. Исследовать, как с помощью renderer.snapshot можно создать эффект "заморозки кадра" для меню паузы