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

При работе с графикой в Phaser часто возникает ситуация, когда у вас есть единое изображение тайлсета, но для отображения отдельных спрайтов требуется спрайтшит — текстура с разбивкой на кадры. Обычно это решается предзагрузкой отдельного файла или использованием тайловой карты, но что если нужно сделать это динамически, прямо во время выполнения? В этой статье мы разберем официальный пример из репозитория 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('fantasy', 'assets/tilemaps/tiles/fantasy-tiles.png');
    }

    create ()
    {
        const tiles = this.textures.get('fantasy');
        const base = tiles.get();

        Phaser.Textures.Parsers.SpriteSheet(tiles, base.sourceIndex, base.x, base.y, base.width, base.height, {
            frameWidth: 64,
            frameHeight: 64
        });

        this.add.sprite(200, 300, 'fantasy', 6);
        this.add.sprite(300, 300, 'fantasy', 24);
        this.add.sprite(400, 300, 'fantasy', 28);
        this.add.sprite(500, 300, 'fantasy', 31);
        this.add.sprite(600, 300, 'fantasy', 49);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Зачем это нужно? Проблема тайлсетов и спрайтов

В Phaser существует четкое различие между текстурой (объект Texture) и спрайтом (объект Sprite). Спрайт для своей работы часто использует спрайтшит — текстуру, разбитую на отдельные кадры (frames).

Тайлсет, загруженный как обычное изображение (this.load.image), по умолчанию является текстурой с одним кадром. Если попытаться создать спрайт, указав номер кадра (например, 'fantasy', 6), движок не найдет этот кадр и, в лучшем случае, отобразит весь тайлсет целиком, а в худшем — ничего.

Классическое решение — заранее подготовить JSON-конфигурацию с разметкой кадров или использовать тайловую карту (Tilemap). Но иногда требуется гибкость: например, вы загружаете тайлсет с сервера и вам нужно на лету определить его структуру кадров.

Сердце метода: парсер SpriteSheet

Ключ к решению — статический метод Phaser.Textures.Parsers.SpriteSheet. Этот внутренний парсер движка обычно используется за кулисами при загрузке спрайтшитов, но его можно вызвать вручную для любой существующей текстуры.

Метод принимает на вход текстуру и параметры разбивки, а затем модифицирует эту текстуру, добавляя в нее кадры согласно заданной сетке.

Phaser.Textures.Parsers.SpriteSheet(tiles, base.sourceIndex, base.x, base.y, base.width, base.height, {
    frameWidth: 64,
    frameHeight: 64
});

В этом вызове: - tiles — объект текстуры, полученный через this.textures.get('fantasy'). - base — базовый исходный кадр текстуры (обычно это и есть всё изображение). Мы передаем его координаты (`x,y) и размеры (width,height`). - Последний аргумент — конфигурация, где frameWidth и frameHeight задают размер одного кадра (тайла) в пикселях.

После выполнения этого кода текстура 'fantasy' перестает быть монолитным изображением и становится полноценным спрайтшитом.

Пошаговый разбор кода примера

Давайте пройдемся по коду примера от загрузки до отображения.

**1. Загрузка изображения как обычной текстуры:**

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('fantasy', 'assets/tilemaps/tiles/fantasy-tiles.png');
}

На этом этапе изображение загружено в кеш текстур под ключом 'fantasy', но оно еще не является спрайтшитом.

**2. Получение объекта текстуры и её базового кадра:**

create ()
{
    const tiles = this.textures.get('fantasy');
    const base = tiles.get();

this.textures.get() возвращает объект класса Texture. Метод texture.get() без аргументов возвращает её первый (и на данный момент единственный) кадр — объект Frame.

**3. Преобразование текстуры:** Код вызова парсера мы уже разобрали выше. После его выполнения текстура содержит сетку кадров 64x64 пикселя.

**4. Создание спрайтов с использованием кадров:**

this.add.sprite(200, 300, 'fantasy', 6);
    this.add.sprite(300, 300, 'fantasy', 24);
    // ... и так далее

Теперь, когда текстура 'fantasy' стала спрайтшитом, мы можем создавать спрайты, указывая в четвертом параметре индекс нужного кадра. Индексация кадров обычно идет слева направо, сверху вниз.

Важные нюансы и ограничения

Используя этот метод, важно помнить о нескольких моментах:

1. **Необратимость:** Парсер SpriteSheet мутирует исходную текстуру. Если она использовалась где-то еще как цельное изображение (например, как фон), это перестанет работать.

2. **Размеры изображения:** Убедитесь, что ширина и высота изображения кратны frameWidth и frameHeight. Иначе парсер создаст неполные кадры по краям.

3. **Ключ текстуры:** Вы работаете с уже существующей в кеше текстурой по её строковому ключу. После парсинга все спрайты, использующие этот ключ, будут обращаться к новому спрайтшиту.

4. **Анимации:** После создания спрайтшита вы можете создавать анимации стандартными средствами Phaser, используя индексы кадров.

// Пример создания анимации после парсинга
this.anims.create({
    key: 'walk',
    frames: this.anims.generateFrameNumbers('fantasy', { start: 0, end: 5 }),
    frameRate: 10,
    repeat: -1
});

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

Ручной вызов Phaser.Textures.Parsers.SpriteSheet — это мощный инструмент для продвинутой работы с графикой в Phaser. Он стирает грань между статическими тайлсетами и анимированными спрайтшитами, позволяя гибко управлять ассетами во время выполнения игры. **Идеи для экспериментов:** 1. Загрузите несколько тайлсетов и скомбинируйте их в один виртуальный спрайтшит, создав новую пустую текстуру (this.textures.createCanvas) и нарисовав на ней выбранные кадры. 2. Реализуйте динамическую подгрузку графики с сервера, где конфигурация кадров (размер и, возможно, порядок) приходит отдельным JSON-файлом. 3. Используйте этот метод в редакторе уровней, чтобы позволить пользователю выбирать тайлы из любого загруженного изображения, просто указав размер сетки.