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

Загрузка спрайтов через внешние JSON-файлы — стандартный подход в Phaser. Но что, если нужно встроить данные атласа прямо в код игры? Например, для прототипирования, создания демо или уменьшения количества сетевых запросов. В этом примере показано, как использовать объект JSON, объявленный прямо в классе сцены, для создания текстурного атласа. Это мощный приём для гибкой работы с ресурсами. Мы разберём, как объявить данные атласа в виде свойства класса, передать их в загрузчик `this.load.atlas` и затем использовать для создания множества спрайтов со случайным позиционированием. Этот метод даёт полный контроль над метаданными текстур прямо из JavaScript.

Версия 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.image('testimage1', 'assets/sprites/block.png'); // this.load.json('test1', 'assets/atlas/megaset-0.json'); this.load.atlas('megaset', 'assets/atlas/megaset-3.png', this.atlasJSON); this.load.image('bob', 'assets/sprites/crate.png'); } 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(0, 600); this.add.image(x, y, 'megaset', frames[i]); } } } const config = { type: Phaser.AUTO, width: 800, height: 600, parent: 'phaser-example', scene: Example }; const game = new Phaser.Game(config);

Объявление атласа как объекта JavaScript

Вместо загрузки внешнего файла, данные атласа (список кадров и мета-информация) объявляются как свойство класса сцены atlasJSON. Это обычный JavaScript-объект, соответствующий структуре JSON, который генерируют такие инструменты, как Texture Packer.

Ключевые части объекта: - frames: Массив объектов, каждый из которых описывает один спрайт (имя файла, координаты и размеры рамки, данные об обрезке и точка поворота). - meta: Метаданные, включая путь к изображению (image) и его общий размер (size).

atlasJSON = {
    frames: [
        {
            filename: 'explosion',
            frame: {x: 2, y: 2, w: 319, h: 312},
            trimmed: true,
            spriteSourceSize: {x: 1, y: 6, w: 319, h: 312},
            sourceSize: {w: 320, h: 320},
            pivot: {x: 0.5, y: 0.5}
        }
        // ... другие кадры
    ],
    meta: {
        image: 'megaset-3.png',
        size: {w: 1023, h: 691}
    }
};

Объект frame определяет прямоугольную область на большом изображении. Поля trimmed и spriteSourceSize критически важны для корректного отображения обрезанных спрайтов.

Загрузка атласа и изображения

В методе preload() происходит настройка и загрузка ресурсов. Сначала базовый URL задаётся через this.load.setBaseURL. Это позволяет указывать относительные пути для остальных ресурсов.

Затем вызывается ключевой метод `this.load.atlas`. Он принимает три аргумента:
1.  Ключ текстуры (`'megaset'`), по которому атлас будет доступен в кэше.
2.  Путь к PNG-изображению, содержащему все спрайты.
3.  Данные атласа. Обычно здесь передаётся путь к JSON-файлу, но в нашем случае — наш JavaScript-объект `this.atlasJSON`.

Отдельно загружается обычное изображение 'bob', чтобы показать, что оба типа загрузки могут работать параллельно.

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

Phaser самостоятельно распознаёт, что третий аргумент — объект, а не строка, и использует его как данные атласа, не пытаясь загрузить его как файл.

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

После завершения загрузки в методе create() мы получаем доступ к загруженной текстуре атласа. Метод this.textures.get('megaset') возвращает объект текстуры. Из него методом .getFrameNames() извлекается массив имён всех кадров (значения filename из нашего JSON).

const atlasTexture = this.textures.get('megaset');
const frames = atlasTexture.getFrameNames(); // ['explosion', 'spaceman', ...]

Случайное размещение спрайтов на сцене

Далее в цикле для каждого имени кадра создаётся спрайт. Координаты `xиyгенерируются случайно в пределах сцены с помощьюPhaser.Math.Between`.

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(0, 600);
        this.add.image(x, y, 'megaset', frames[i]);
    }
}

Метод this.add.image создаёт игровой объект Image. Четвёртый аргумент — имя кадра — указывает, какой именно спрайт из атласа 'megaset' использовать. В результате на сцене появляется хаотичная коллекция всех изображений из атласа.

Конфигурация и запуск игры

Стандартная конфигурация игры Phaser. Важно, что в свойстве scene передаётся класс нашей сцены Example. При создании экземпляра Phaser.Game с этой конфигурацией автоматически вызываются методы preload и create.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: Example // Наш класс сцены с встроенным атласом
};

const game = new Phaser.Game(config);

Эта структура типична для проектов на Phaser 3. Основная логика примера инкапсулирована в классе сцены.

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

Встраивание данных текстурного атласа прямо в код — это эффективный способ для быстрого прототипирования, создания изолированных примеров или сборки всего кода игры в один файл. Вы получаете полный программный контроль над метаданными спрайтов. Для экспериментов попробуйте: 1. Динамически модифицировать объект atlasJSON перед загрузкой (например, менять pivot точек у всех спрайтов). 2. Скомбинировать этот подход с загрузкой: часть атласов грузить из внешних файлов, а критически важные — держать в коде. 3. Сгенерировать простой атлас программно, описав расположение нескольких загруженных по отдельности изображений на виртуальном «холсте».