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

Оптимизация графики — ключ к производительности игр, особенно для веб- и мобильных платформ. В этом примере мы наглядно сравниваем два формата изображений — PNG и новый AVIF — демонстрируя колоссальную разницу в размере файлов при визуально одинаковом качестве. Вы не только увидите, как загружать и отображать форматы нового поколения в Phaser, но и научитесь интерактивно работать с обрезкой текстур, что полезно для создания динамических интерфейсов или эффектов "раскрытия" контента.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('mechPNG', 'assets/pics/equality-by-ragnarok.png');
        this.load.image('mechAVIF', 'assets/pics/equality-by-ragnarok.avif');
    }

    create ()
    {
        // https://jakearchibald.com/2020/avif-has-landed/

        const png = this.add.image(0, 0, 'mechPNG').setScale(0.54).setOrigin(0);
        const avif = this.add.image(0, 0, 'mechAVIF').setScale(0.54).setOrigin(0);

        this.add.text(32, 604, '^ PNG - 2,953 KB');
        this.add.text(992, 604, '^ AVIF - 135 KB').setOrigin(1, 0);

        const debug = this.add.graphics();

        const drawMarker = (x) =>
        {

            debug.clear();
            debug.fillStyle(0x00ff00);
            debug.fillRect(x - 1, 0, 3, 583);
            debug.fillCircle(x, 300, 12);
            debug.fillStyle(0x000000);
            debug.fillTriangle(x - 2, 300 - 6, x - 2, 300 + 6, x - 2 - 6, 300);
            debug.fillTriangle(x + 2, 300 - 6, x + 2, 300 + 6, x + 2 + 6, 300);

        };

        drawMarker(512);

        avif.setCrop(512 * (1 / png.scaleX), 0, 1920, 1080);

        this.input.on('pointermove', (pointer) =>
        {

            drawMarker(pointer.x);

            avif.setCrop(pointer.x * (1 / png.scaleX), 0, 1920, 1080);

        });
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 1024,
    height: 640,
    backgroundColor: '#000000',
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка текстур: PNG и AVIF

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

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('mechPNG', 'assets/pics/equality-by-ragnarok.png');
this.load.image('mechAVIF', 'assets/pics/equality-by-ragnarok.avif');

Ключевой момент здесь — демонстрация размера. PNG-файл весит почти 3 МБ, в то время как его AVIF-аналог — всего 135 КБ. AVIF — современный формат, обеспечивающий высокое качество при сильном сжатии, что напрямую влияет на скорость загрузки вашей игры.

Создание и позиционирование изображений

В create() мы создаём два объекта изображения, используя загруженные текстуры. Оба изображения размещаются в точке (0,0) — левом верхнем углу сцены. Масштаб 0.54 применяется, чтобы картинки поместились в область просмотра (viewport). Установка setOrigin(0) фиксирует точку привязки (origin) изображения в его левом верхнем углу, что упрощает последующие вычисления для обрезки.

const png = this.add.image(0, 0, 'mechPNG').setScale(0.54).setOrigin(0);
const avif = this.add.image(0, 0, 'mechAVIF').setScale(0.54).setOrigin(0);

Затем добавляются текстовые метки, которые отображают размер каждого файла. Это наглядно для пользователя, запустившего пример.

Интерактивная обрезка (Crop) и визуальный маркер

Сердце примера — интерактивное сравнение. Изначально для AVIF-изображения устанавливается обрезка (setCrop), которая скрывает его левую часть. Метод setCrop(x, y, width, height) определяет прямоугольную область текстуры для отображения.

avif.setCrop(512 * (1 / png.scaleX), 0, 1920, 1080);

Поскольку изображение масштабировано, координата обрезки по X (512) умножается на обратный масштаб (1 / 0.54). Это преобразует координату из пространства отмасштабированного спрайта обратно в пространство исходной текстуры (1920x1080).

Для наглядности создаётся зелёный маркер с помощью графического объекта (add.graphics). Функция drawMarker рисует вертикальную линию и стрелки, указывающие на границу обрезки.

const drawMarker = (x) => {
    debug.clear();
    debug.fillStyle(0x00ff00);
    debug.fillRect(x - 1, 0, 3, 583);
    // ... рисование круга и треугольников
};

Обработка движения указателя

Интерактивность добавляется через слушатель события pointermove. При каждом движении мыши или касании вычисляется новая позиция маркера и соответственно меняется область обрезки AVIF-изображения.

this.input.on('pointermove', (pointer) => {
    drawMarker(pointer.x);
    avif.setCrop(pointer.x * (1 / png.scaleX), 0, 1920, 1080);
});

Таким образом, пользователь, перемещая маркер, "раскрывает" AVIF-версию изображения и может в реальном времени сравнить её качество с PNG. Это мощный приём для демонстрации контента или создания интерактивных элементов интерфейса, где часть изображения должна открываться постепенно.

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

Пример наглядно доказывает эффективность формата AVIF для оптимизации ресурсов в играх Phaser без потери визуального качества. Техника динамического кропинга с помощью setCrop() открывает двери для множества экспериментов: создание интерактивных слайдеров "до/после", реализация эффектов постепенного проявления спрайтов (например, для затуманенной карты), или построение нестандартных UI-элементов, где видимая часть изображения управляется жестами игрока. Попробуйте применить этот подход к анимированным текстурам (spritesheet) или комбинировать его с масками для более сложных визуальных эффектов.