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

При разработке игр часто возникает необходимость создавать анимации из кадров, упакованных в общую текстуру — атлас. 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.path = 'assets/atlas/trimsheet/';

        this.load.atlas('testanims', 'trimsheet.png', 'trimsheet.json');
    }

    create ()
    {
        //  create a sprite sheet from a frame embedded in a texture atlas
        //  'boom' is the unique local name we'll give the sprite sheet
        //  'megaset' is the key of the texture atlas that contains the sprite sheet
        //  'explosion' is the name of the frame within the texture atlas
        //  The rest of the values are the sprite sheet frame sizes and offsets

        const t1 = this.textures.addSpriteSheetFromAtlas(
            'boom1',
            {
                atlas: 'testanims',
                frame: 'explosion-notrim',
                frameWidth: 64,
                frameHeight: 64,
                endFrame: 23
            });


        const t2 = this.textures.addSpriteSheetFromAtlas(
            'boom2',
            {
                atlas: 'testanims',
                frame: 'explosion',
                frameWidth: 64,
                frameHeight: 64,
                endFrame: 23
            });

        const b1 = this.textures.addSpriteSheetFromAtlas(
            'bubble1',
            {
                atlas: 'testanims',
                frame: 'bubble-notrim',
                frameWidth: 34,
                frameHeight: 68
            });


        const b2 = this.textures.addSpriteSheetFromAtlas(
            'bubble2',
            {
                atlas: 'testanims',
                frame: 'bubble',
                frameWidth: 34,
                frameHeight: 68
            });

        //  There is a new texture available called 'boom1', which we can assign to game objects:

        const config1 = {
            key: 'explode1',
            frames: this.anims.generateFrameNumbers('boom1', { start: 0, end: 23, first: 23 }),
            frameRate: 20,
            repeat: -1
        };

        const config2 = {
            key: 'explode2',
            frames: this.anims.generateFrameNumbers('boom2', { start: 0, end: 23, first: 23 }),
            frameRate: 20,
            repeat: -1
        };

        const config3 = {
            key: 'bobble1',
            frames: this.anims.generateFrameNumbers('bubble1', { start: 0, end: 6 }),
            frameRate: 10,
            repeat: -1
        };

        const config4 = {
            key: 'bobble2',
            frames: this.anims.generateFrameNumbers('bubble2', { start: 0, end: 6 }),
            frameRate: 10,
            repeat: -1
        };

        this.anims.create(config1);
        this.anims.create(config2);
        this.anims.create(config3);
        this.anims.create(config4);

        this.add.sprite(300, 200).play('explode1');
        this.add.sprite(400, 200).play('explode2');

        this.add.sprite(300, 400).play('bobble1');
        this.add.sprite(400, 400).play('bobble2');
    }
}

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

const game = new Phaser.Game(config);

Загрузка текстурного атласа

Перед работой с кадрами необходимо загрузить сам атлас. В методе preload мы используем this.load.atlas. Этот метод загружает PNG-изображение и JSON-файл с координатами и размерами каждого фрейма внутри атласа.

this.load.atlas('testanims', 'trimsheet.png', 'trimsheet.json');

Ключ 'testanims' — это уникальное имя, по которому мы будем обращаться к атласу в коде. Phaser автоматически парсит JSON и создает внутреннюю структуру фреймов.

Метод addSpriteSheetFromAtlas

Для создания нового спрайт-листа на основе одного фрейма из атласа используется метод this.textures.addSpriteSheetFromAtlas. Этот метод не загружает новые файлы, а создает виртуальную текстуру, «нарезая» указанный регион атласа на кадры заданного размера.

const t1 = this.textures.addSpriteSheetFromAtlas(
    'boom1',
    {
        atlas: 'testanims',
        frame: 'explosion-notrim',
        frameWidth: 64,
        frameHeight: 64,
        endFrame: 23
    });

Первый аргумент — 'boom1' — это уникальный ключ для новой текстуры. В объекте конфигурации: - atlas: ключ ранее загруженного атласа. - frame: имя конкретного фрейма внутри атласа, который содержит всю последовательность анимации. - frameWidth и frameHeight: размер каждого отдельного кадра внутри этого фрейма. - endFrame: опциональный параметр, указывающий последний кадр для извлечения. Если не указан, будет взята вся последовательность до конца фрейма атласа.

Таким образом, из одного большого изображения 'explosion-notrim' мы создаем спрайт-лист из 24 кадров (от 0 до 23) размером 64x64 пикселей каждый.

Создание анимаций из новых текстур

После создания текстуры 'boom1' мы можем использовать её для генерации анимации. Для этого применяется стандартный подход Phaser: создание конфигурации анимации с помощью this.anims.generateFrameNumbers и регистрация через this.anims.create.

const config1 = {
    key: 'explode1',
    frames: this.anims.generateFrameNumbers('boom1', { start: 0, end: 23, first: 23 }),
    frameRate: 20,
    repeat: -1
};
this.anims.create(config1);

this.anims.generateFrameNumbers принимает ключ текстуры ('boom1') и объект с параметрами start, end и first. Параметр first: 23 указывает, с какого кадра начинать воспроизведение анимации — в данном случае с последнего, чтобы анимация взрыва проигрывалась от конца к началу. repeat: -1 задает бесконечное повторение.

Разница между обрезанными и необрезанными фреймами

В исходном примере создаются две пары текстур: boom1/boom2 и bubble1/bubble2. Они используют разные исходные фреймы из одного атласа: 'explosion-notrim' и 'explosion', 'bubble-notrim' и 'bubble'.

const t1 = this.textures.addSpriteSheetFromAtlas('boom1', {
    atlas: 'testanims',
    frame: 'explosion-notrim', // Необрезанный фрейм
    frameWidth: 64,
    frameHeight: 64,
    endFrame: 23
});

const t2 = this.textures.addSpriteSheetFromAtlas('boom2', {
    atlas: 'testanims',
    frame: 'explosion', // Обрезанный фрейм
    frameWidth: 64,
    frameHeight: 64,
    endFrame: 23
});

Фреймы с суффиксом -notrim содержат исходные изображения с прозрачными областями (padding), которые были добавлены при упаковке в атлас. Фреймы без суффикса уже были обрезаны (trimmed) — из них удалены однородные прозрачные края. При создании спрайт-листа метод addSpriteSheetFromAtlas корректно работает с обоими типами, но визуально анимация может отличаться выравниванием. На практике использование обрезанных фреймов экономит видеопамять и может улучшить производительность.

Отображение спрайтов с анимацией

После создания анимаций их можно присвоить игровым объектам. В примере создаются спрайты без начальной текстуры, и сразу запускается анимация с помощью метода .play().

this.add.sprite(300, 200).play('explode1');
this.add.sprite(400, 200).play('explode2');

this.add.sprite(x, y) создает спрайт в указанных координатах. Поскольку мы не передали ключ текстуры, спрайт будет пустым до начала анимации. Вызов .play('explode1') запускает ранее созданную анимацию с ключом 'explode1'. Phaser автоматически подставляет соответствующие кадры из текстуры 'boom1'. Это удобно для динамического создания эффектов.

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

Метод addSpriteSheetFromAtlas — мощный инструмент для работы с упакованными ассетами. Он позволяет выделять отдельные анимации из больших атласов, создавая независимые текстуры без дублирования ресурсов. Для экспериментов попробуйте: 1. Изменять параметры frameWidth и frameHeight, чтобы «нарезать» фреймы по-разному. 2. Использовать разные значения start и end в generateFrameNumbers для создания укороченных или перетасованных анимаций. 3. Динамически создавать спрайт-листы на основе состояния игры, например, для procedural animation.