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

Загрузка готовых спрайтшитов — стандартный подход, но Phaser 3 позволяет создавать анимационные кадры программно, прямо во время выполнения. Это открывает двери для процедурной генерации эффектов, динамических интерфейсов или уникальной графики, которая меняется в зависимости от игрового процесса. В этой статье мы разберем пример, где все кадры анимации рисуются на Canvas и тут же превращаются в текстурный атлас и плавную анимацию.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        // this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
        const framesPerRow = 8;
        const frameTotal = 32;

        //  Create a CanvasTexture that is 256 x 128 in size.
        //  The frames will be 32 x 32, which means we'll fit in 8 x 4 of them to our texture size, for a total of 32 frames.
        const canvasFrame = this.textures.createCanvas('dynamicFrames', 256, 128);

        let radius = 0;
        const radiusInc = 16 / frameTotal;

        let x = 0;
        let y = 0;
        let ctx = canvasFrame.context;

        ctx.fillStyle = '#ffff00';

        for (let i = 1; i <= frameTotal; i++)
        {
            //  Draw an arc to the CanvasTexture
            ctx.beginPath();
            ctx.arc(x + 16, y + 16, Math.max(1, radius), 0, Math.PI * 2, false);
            ctx.closePath();
            ctx.fill();

            //  Now we add a frame to the CanvasTexture.
            //  See the docs for the arguments.
            canvasFrame.add(i, 0, x, y, 32, 32);

            x += 32;
            radius += radiusInc;

            //  Hit the end of the row? Wrap it around.
            if (i % framesPerRow === 0)
            {
                x = 0;
                y += 32;
            }
        }

        //  Call this if running under WebGL, or you'll see nothing change
        canvasFrame.refresh();

        //  Display the whole of our freshly baked sprite sheet
        this.add.image(0, 0, 'dynamicFrames', '__BASE').setOrigin(0);

        //  Let's create an animation from the new frames
        this.anims.create({
            key: 'pulse',
            frames: this.anims.generateFrameNumbers('dynamicFrames', { start: 1, end: frameTotal }),
            frameRate: 28,
            repeat: -1,
            yoyo: true
        });

        //  Add a bunch of Sprites that all use the same base texture and animation
        for (let i = 0; i < 50; i++)
        {
            const ball = this.add.sprite(8 + i * 16, 164, 'dynamicFrames').play('pulse');

            this.tweens.add({
                targets: ball,
                y: 584,
                duration: 2000,
                ease: 'Quad.easeInOut',
                delay: i * 30,
                yoyo: true,
                repeat: -1
            });
        }
    }
}

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

const game = new Phaser.Game(config);

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

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

const canvasFrame = this.textures.createCanvas('dynamicFrames', 256, 128);

Метод this.textures.createCanvas создает объект CanvasTexture с именем 'dynamicFrames' и размерами 256x128 пикселей. В эту область мы будем рисовать отдельные кадры.

Рисование кадров и определение фреймов

В цикле мы рисуем графику на Canvas и регистрируем каждый рисунок как отдельный фрейм в текстуре.

ctx.beginPath();
ctx.arc(x + 16, y + 16, Math.max(1, radius), 0, Math.PI * 2, false);
ctx.fill();

canvasFrame.add(i, 0, x, y, 32, 32);

Сначала ctx.arc рисует круг. Его радиус (radius) постепенно увеличивается, создавая эффект "пульсации" от кадра к кадру. Затем критически важный вызов canvasFrame.add добавляет область нарисованного канваса как отдельный фрейм в текстуру. Аргументы: уникальный индекс фрейма, индекс источника (0 для единственного), и координаты X, Y, ширина и высота области (32x32). После заполнения строки из 8 фреймов, координата Y сдвигается, начиная новую строку.

canvasFrame.refresh();

При работе под WebGL необходимо вызвать refresh(), чтобы изменения на Canvas были загружены в видеопамять (GPU). Под Canvas-рендерером этот вызов не обязателен, но его наличие сделает код универсальным.

Создание анимации из сгенерированных фреймов

Теперь, когда текстура содержит 32 пронумерованных фрейма, мы можем создать из них анимацию стандартными средствами Phaser.

this.anims.create({
    key: 'pulse',
    frames: this.anims.generateFrameNumbers('dynamicFrames', { start: 1, end: frameTotal }),
    frameRate: 28,
    repeat: -1,
    yoyo: true
});

Функция this.anims.generateFrameNumbers генерирует массив кадров для анимации, используя имена нашей динамической текстуры ('dynamicFrames') и диапазон индексов (с 1 по 32). Ключевые параметры анимации: frameRate (скорость), repeat: -1 (бесконечное повторение) и yoyo: true (анимация проигрывается вперед-назад).

Использование анимации и добавление движения

Мы создаем несколько спрайтов, которые используют одну текстуру и одну анимацию, но оживляем их независимыми движениями.

const ball = this.add.sprite(8 + i * 16, 164, 'dynamicFrames').play('pulse');

this.tweens.add({
    targets: ball,
    y: 584,
    duration: 2000,
    ease: 'Quad.easeInOut',
    delay: i * 30,
    yoyo: true,
    repeat: -1
});

Спрайт создается с использованием нашей текстуры 'dynamicFrames'. Вызов .play('pulse') сразу запускает на нем созданную анимацию пульсации круга. Затем для каждого спрайта создается Tween, который двигает его по вертикали с задержкой (delay: i * 30), создавая волнообразный эффект. Комбинация внутренней анимации (play) и внешнего движения (tween) рождает сложный визуал при минимальном коде.

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

Этот подход стирает грань между кодом и контентом. Вы можете генерировать кадры на основе математических формул, данных игрока или случайных значений. Поэкспериментируйте: измените форму в ctx.arc на ctx.rect, генерируйте градиенты или шум, используйте разные алгоритмы для изменения радиуса. Такой метод идеален для создания уникальных эффектов частиц, динамических фонов или индикаторов состояния, которые невозможно заранее нарисовать в графическом редакторе.