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

Создание скриншотов (снимков экрана) прямо во время работы игры — мощный инструмент для отладки, создания систем фоторежима или просто для веселья. В этом примере мы разберем, как использовать встроенный метод `snapshot` рендерера Phaser, а также реализуем простую систему рисования на холсте игры с помощью мыши. Вы научитесь захватывать текущий кадр в изображение и добавлять на него интерактивные графические элементы.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    isKeyDown = false;
    isMouseDown = false;
    graphicsPath = [];
    graphics;
    snapHistory = [];
    time = 0;
    div = document.createElement('div');

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('myImage', 'assets/sprites/phaser1.png');
        this.load.glsl('shader0', 'assets/shaders/shader0.frag');
    }

    create ()
    {
        this.div.innerHTML = 'PRESS SPACE TO TAKE SNAPSHOT<br>';
        document.body.appendChild(this.div);

        for (let i = 0; i < 5; ++i)
        {
            const image = this.add.image(Math.random() * 800, Math.random() * 600, 'myImage');
        }

        this.graphics = this.add.graphics({x: 0, y: 0});

        game.canvas.onmousedown = e =>
        {
            this.isMouseDown = true;
            this.graphics.clear();
            this.graphicsPath.length = 0;
        };
        game.canvas.onmouseup = e =>
        {
            this.isMouseDown = false;
        };
        game.canvas.onmousemove = e =>
        {
            const mouseX = e.clientX - game.canvas.offsetLeft;
            const mouseY = e.clientY - game.canvas.offsetTop;
            if (this.isMouseDown)
            { this.graphicsPath.push({x: mouseX, y: mouseY}); }
        };
        window.onkeydown = e =>
        {
            if (e.keyCode === 32 && !this.isKeyDown)
            {
                game.renderer.snapshot(image =>
                {
                    image.style.width = '160px';
                    image.style.height = '120px';
                    image.style.paddingLeft = '2px';
                    this.snapHistory.push(image);
                    console.log('snap!');
                    document.body.appendChild(image);
                });
                this.isKeyDown = true;
            }
        };
        window.onkeyup = e =>
        {
            if (e.keyCode === 32)
            {
                this.isKeyDown = false;
            }
        };
    }

    update ()
    {
        const length = this.graphicsPath.length;

        this.graphics.clear();
        this.graphics.lineStyle(10.0, 0xFFFF00, 1.0);
        this.graphics.beginPath();
        for (let i = 0; i < length; ++i)
        {
            const node = this.graphicsPath[i];

            if (i !== 0)
            {
                this.graphics.lineTo(node.x, node.y);
            }
            else
            {
                this.graphics.moveTo(node.x, node.y);
            }
        }
        this.graphics.strokePath();
        this.graphics.closePath();

        this.time += 0.01;
    }
}

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

const game = new Phaser.Game(config);

Инициализация сцены и загрузка ресурсов

В методе preload() загружаются необходимые ресурсы: спрайт и шейдер. Обратите внимание на использование this.load.setBaseURL() для указания базового пути, что удобно для загрузки удаленных ассетов из репозитория с примерами.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('myImage', 'assets/sprites/phaser1.png');
    this.load.glsl('shader0', 'assets/shaders/shader0.frag');
}

В create() происходит основная настройка: создается текстовый блок для инструкций, добавляются несколько спрайтов в случайных позициях и инициализируется объект Graphics для рисования. Объект this.graphics будет использоваться для отображения линии, которую пользователь рисует мышью. Также здесь навешиваются обработчики событий мыши и клавиатуры непосредственно на глобальные объекты game.canvas и window.

Обработка ввода: рисуем мышью

Для реализации рисования используются стандартные DOM-события мыши, привязанные к холсту игры. Это демонстрирует, как можно комбинировать Phaser с нативным API браузера.

game.canvas.onmousedown = e =>
{
    this.isMouseDown = true;
    this.graphics.clear();
    this.graphicsPath.length = 0;
};

При нажатии кнопки мыши (onmousedown) флаг isMouseDown устанавливается в true, а текущий графический путь очищается. Массив graphicsPath служит для хранения точек линии.

game.canvas.onmousemove = e =>
{
    const mouseX = e.clientX - game.canvas.offsetLeft;
    const mouseY = e.clientY - game.canvas.offsetTop;
    if (this.isMouseDown)
    { this.graphicsPath.push({x: mouseX, y: mouseY}); }
};

При движении мыши с зажатой кнопкой координаты (mouseX, mouseY) вычисляются относительно холста и добавляются в массив graphicsPath. Это и есть наш «след» от мыши.

Создание скриншота с помощью `game.renderer.snapshot`

Ключевая функциональность — создание снимка текущего состояния рендерера. Это делается при нажатии клавиши Пробел (код 32).

window.onkeydown = e =>
{
    if (e.keyCode === 32 && !this.isKeyDown)
    {
        game.renderer.snapshot(image =>
        {
            image.style.width = '160px';
            image.style.height = '120px';
            image.style.paddingLeft = '2px';
            this.snapHistory.push(image);
            console.log('snap!');
            document.body.appendChild(image);
        });
        this.isKeyDown = true;
    }
};

Метод game.renderer.snapshot() принимает коллбэк, в который передается готовый HTMLImageElement. В этом коллбэке мы настраиваем стили изображения (уменьшаем размер), добавляем его в массив истории snapHistory и вставляем прямо в тело документа. Обратите внимание на флаг isKeyDown, который предотвращает множественные срабатывания при зажатой клавише.

Отображение графического пути в `update()`

Чтобы нарисованная мышью линия отображалась в кадре игры (и, соответственно, попадала на скриншоты), ее необходимо перерисовывать каждый кадр в методе update(). Используется API объекта Graphics.

this.graphics.clear();
this.graphics.lineStyle(10.0, 0xFFFF00, 1.0);
this.graphics.beginPath();
for (let i = 0; i < length; ++i)
{
    const node = this.graphicsPath[i];
    if (i !== 0)
    {
        this.graphics.lineTo(node.x, node.y);
    }
    else
    {
        this.graphics.moveTo(node.x, node.y);
    }
}
this.graphics.strokePath();
this.graphics.closePath();

Каждый кадр графический объект очищается, устанавливается стиль линии (толщина 10, желтый цвет), и затем по точкам из graphicsPath строится ломаная линия. moveTo задает начальную точку, а lineTo — последующие. strokePath() выполняет отрисовку. Это классический подход для рисования динамических линий в Phaser.

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

Вы изучили практический пример создания интерактивного скриншотера с рисованием в Phaser. Ключевые элементы: метод snapshot рендерера для захвата кадра, работа с DOM-событиями для ввода и объект Graphics для динамической векторной графики. Для экспериментов попробуйте: сохранять скриншоты не в DOM, а на сервер; рисовать не линию, а фигуры; применять к скриншотам фильтры из загруженного шейдера shader0; или интегрировать систему скриншотов в игровое меню.