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

Texture Atlas — один из самых эффективных способов управления игровыми спрайтами, объединяющий множество изображений в один файл с JSON-описанием. Часто в JSON-файле атласа хранится не только информация о кадрах, но и полезные метаданные (версия, инструмент создания, параметры). В этой статье мы разберем, как в Phaser загрузить атлас с собственным JSON-объектом, получить доступ ко всем его кадрам, а также извлечь и отобразить дополнительные метаданные, встроенные в JSON. Этот подход полезен для динамического создания объектов, отладки и организации ресурсов.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    atlasJSON = {
        frames: [
            {
                filename: '128x128',
                frame: {x: 893,y: 342,w: 128,h: 128},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 128,h: 128},
                sourceSize: {w: 128,h: 128},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'advanced_wars_land',
                frame: {x: 132,y: 641,w: 320,h: 48},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 320,h: 48},
                sourceSize: {w: 320,h: 48},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'contra2',
                frame: {x: 2,y: 316,w: 142,h: 222},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 142,h: 222},
                sourceSize: {w: 142,h: 222},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'contra3',
                frame: {x: 645,y: 197,w: 246,h: 201},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 246,h: 201},
                sourceSize: {w: 246,h: 201},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'diamonds32x5',
                frame: {x: 585,y: 596,w: 318,h: 49},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 1,y: 15,w: 318,h: 49},
                sourceSize: {w: 320,h: 64},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'exocet_spaceman',
                frame: {x: 146,y: 316,w: 153,h: 175},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 153,h: 175},
                sourceSize: {w: 153,h: 175},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'explosion',
                frame: {x: 2,y: 2,w: 319,h: 312},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 1,y: 6,w: 319,h: 312},
                sourceSize: {w: 320,h: 320},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'helix',
                frame: {x: 724,y: 472,w: 221,h: 28},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 6,y: 0,w: 221,h: 28},
                sourceSize: {w: 233,h: 30},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'knight3',
                frame: {x: 323,y: 204,w: 320,h: 131},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 0,y: 0,w: 320,h: 131},
                sourceSize: {w: 320,h: 200},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'lance-overdose-loader-eye',
                frame: {x: 2,y: 540,w: 128,h: 128},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 128,h: 128},
                sourceSize: {w: 128,h: 128},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'mask-test',
                frame: {x: 323,y: 2,w: 320,h: 200},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 320,h: 200},
                sourceSize: {w: 320,h: 200},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'metalslug_monster39x40',
                frame: {x: 436,y: 337,w: 156,h: 160},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 156,h: 160},
                sourceSize: {w: 156,h: 160},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'pacman_by_oz_28x28',
                frame: {x: 454,y: 647,w: 308,h: 28},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 308,h: 28},
                sourceSize: {w: 308,h: 28},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'parsec',
                frame: {x: 281,y: 527,w: 302,h: 80},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 302,h: 80},
                sourceSize: {w: 302,h: 80},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'profil-sad-plush',
                frame: {x: 146,y: 493,w: 133,h: 142},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 133,h: 142},
                sourceSize: {w: 133,h: 142},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'saw',
                frame: {x: 594,y: 400,w: 128,h: 128},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 128,h: 128},
                sourceSize: {w: 128,h: 128},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'shocktroopers-lulu2',
                frame: {x: 301,y: 337,w: 133,h: 188},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 133,h: 188},
                sourceSize: {w: 133,h: 188},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'snowflakes_large',
                frame: {x: 585,y: 530,w: 379,h: 64},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 2,y: 0,w: 379,h: 64},
                sourceSize: {w: 384,h: 64},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'spaceman',
                frame: {x: 724,y: 502,w: 225,h: 16},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 15,y: 0,w: 225,h: 16},
                sourceSize: {w: 240,h: 16},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'steelpp-font',
                frame: {x: 645,y: 2,w: 320,h: 193},
                rotated: false,
                trimmed: true,
                spriteSourceSize: {x: 0,y: 0,w: 320,h: 193},
                sourceSize: {w: 320,h: 200},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'treasure_trap',
                frame: {x: 893,y: 197,w: 127,h: 143},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 127,h: 143},
                sourceSize: {w: 127,h: 143},
                pivot: {x: 0.5,y: 0.5}
            },
            {
                filename: 'vu',
                frame: {x: 281,y: 609,w: 300,h: 30},
                rotated: false,
                trimmed: false,
                spriteSourceSize: {x: 0,y: 0,w: 300,h: 30},
                sourceSize: {w: 300,h: 30},
                pivot: {x: 0.5,y: 0.5}
            } ],
        meta: {
            app: 'http://www.codeandweb.com/texturepacker',
            version: '1.0',
            image: 'megaset-3.png',
            format: 'RGBA8888',
            size: {w: 1023,h: 691},
            scale: '1',
            smartupdate: '$TexturePacker:SmartUpdate:5e8f90752cfd57d3adfb39bcd3eef1b6:87d98cec6fa616080f731b87726d6a1e:b55588eba103b49b35a0a59665ed84fd
#39; } }; preload () { this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/'); this.load.atlas('megaset', 'assets/atlas/megaset-3.png', this.atlasJSON); } create () { const atlasTexture = this.textures.get('megaset'); const frames = atlasTexture.getFrameNames(); for (let i = 0; i < frames.length; i++) { const x = Phaser.Math.Between(0, 800); const y = Phaser.Math.Between(100, 500); this.add.image(x, y, 'megaset', frames[i]).setOrigin(0.5, 0); } // We can access any extra data from the texture atlas json via the Texture.customData property: const str = [ `app: ${atlasTexture.customData.meta.app}`, `version: ${atlasTexture.customData.meta.version}`, `image: ${atlasTexture.customData.meta.image}`, `format: ${atlasTexture.customData.meta.format}`, `scale: ${atlasTexture.customData.meta.scale}` ]; this.add.text(10, 10, str, { font: '16px Courier', fill: '#00ff00' }); } } const config = { type: Phaser.AUTO, width: 800, height: 600, parent: 'phaser-example', scene: Example }; const game = new Phaser.Game(config);

Определение JSON-атласа прямо в коде

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

Объект atlasJSON содержит два основных свойства: frames (массив с данными каждого кадра) и meta (метаданные всего атласа). Каждый элемент в frames включает имя файла (filename), координаты и размеры области в текстуре (frame), флаги обрезки и поворота, а также точку поворота (pivot).

atlasJSON = {
    frames: [
        {
            filename: '128x128',
            frame: {x: 893, y: 342, w: 128, h: 128},
            rotated: false,
            trimmed: false,
            spriteSourceSize: {x: 0, y: 0, w: 128, h: 128},
            sourceSize: {w: 128, h: 128},
            pivot: {x: 0.5, y: 0.5}
        }
        // ... другие кадры
    ],
    meta: {
        app: 'http://www.codeandweb.com/texturepacker',
        version: '1.0',
        image: 'megaset-3.png',
        format: 'RGBA8888',
        size: {w: 1023, h: 691},
        scale: '1'
    }
};

Загрузка атласа с пользовательским JSON

В методе preload используется this.load.atlas для загрузки атласа. Первый параметр — ключ текстуры ('megaset'), второй — путь к изображению, а третий — наш объект atlasJSON. Phaser не требует отдельного файла, он может принять готовый JavaScript-объект, что и происходит здесь.

Также устанавливается базовый URL для загрузки ресурсов с помощью this.load.setBaseURL. Это полезно, чтобы не указывать полные пути для каждого ресурса.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.atlas('megaset', 'assets/atlas/megaset-3.png', this.atlasJSON);
}

Получение списка кадров и создание спрайтов

После загрузки в методе create мы получаем текстуру атласа по ключу с помощью this.textures.get('megaset'). У текстуры есть метод getFrameNames(), который возвращает массив имён всех кадров, определённых в JSON (значения filename).

Затем в цикле для каждого имени кадра создаётся изображение (this.add.image). Позиция задаётся случайно через Phaser.Math.Between. Важно: четвёртый параметр this.add.image — это именно имя кадра из атласа. Метод setOrigin(0.5, 0) устанавливает точку вращения в центре по X и сверху по Y.

const atlasTexture = this.textures.get('megaset');
const frames = atlasTexture.getFrameNames();

for (let i = 0; i < frames.length; i++)
{
    const x = Phaser.Math.Between(0, 800);
    const y = Phaser.Math.Between(100, 500);
    this.add.image(x, y, 'megaset', frames[i]).setOrigin(0.5, 0);
}

Доступ к метаданным атласа через customData

Одна из ключевых возможностей — доступ к исходным данным JSON через свойство customData текстуры. В нашем примере это используется для извлечения информации из объекта meta, который мог быть добавлен инструментом упаковки, например, TexturePacker.

Мы формируем массив строк str с выбранными метаданными и отображаем его на экране зелёным моноширинным текстом.

const str = [
    `app: ${atlasTexture.customData.meta.app}`,
    `version: ${atlasTexture.customData.meta.version}`,
    `image: ${atlasTexture.customData.meta.image}`,
    `format: ${atlasTexture.customData.meta.format}`,
    `scale: ${atlasTexture.customData.meta.scale}`
];

this.add.text(10, 10, str, { font: '16px Courier', fill: '#00ff00' });

Обратите внимание: customData содержит полную структуру переданного JSON, поэтому можно получить доступ не только к meta, но и к любому другому пользовательскому полю, если оно было в исходном объекте.

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

Использование встроенного JSON для Texture Atlas в Phaser даёт гибкость: вы можете генерировать или модифицировать атласы прямо в коде, а также читать любые дополнительные метаданные для логики игры. Это особенно полезно для procedural generation, динамической подгрузки ресурсов или отладки. **Идеи для экспериментов:** 1. Попробуйте динамически изменять объект atlasJSON перед загрузкой, чтобы добавлять или удалять кадры. 2. Используйте метаданные meta для автоматического масштабирования спрайтов под разные разрешения экрана. 3. Создайте интерфейс, который отображает не только метаданные, но и список всех кадров с их параметрами (размер, обрезка) из atlasTexture.customData.frames.