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

В играх плавные переходы между состояниями персонажа — это не просто деталь, а основа качественной анимации. Представьте рыцаря, который не просто резко начинает охранять периметр, а плавно принимает стойку, несколько раз осматривается и возвращается в исходное положение. В Phaser 3 для этого есть мощный инструмент — цепочки анимаций (Animation Chains). Эта статья покажет, как заставить спрайты играть последовательности анимаций без ручного контроля, используя встроенные методы движка.

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('knight', 'assets/animations/knight.png', 'assets/animations/knight.json');
        this.load.image('bg', 'assets/skies/clouds.png');
        this.load.spritesheet('tiles', 'assets/tilemaps/tiles/fantasy-tiles.png', { frameWidth: 64, frameHeight: 64 });
    }

    create ()
    {
        //  The background and floor
        this.add.image(400, 16, 'bg').setOrigin(0.5, 0);

        for (var i = 0; i < 13; i++)
        {
            this.add.image(64 * i, 536, 'tiles', 1).setOrigin(0);
        }

        var text = this.add.text(400, 8, 'Click to play animation chain', { color: '#ffffff' }).setOrigin(0.5, 0);

        //  Our animations
        this.anims.create({
            key: 'guardStart',
            frames: this.anims.generateFrameNames('knight', { prefix: 'guard_start/frame', start: 0, end: 3, zeroPad: 4 }),
            frameRate: 8
        });

        this.anims.create({
            key: 'guard',
            frames: this.anims.generateFrameNames('knight', { prefix: 'guard/frame', start: 0, end: 5, zeroPad: 4 }),
            frameRate: 8,
            repeat: 2
        });

        this.anims.create({
            key: 'guardEnd',
            frames: this.anims.generateFrameNames('knight', { prefix: 'guard_end/frame', start: 0, end: 3, zeroPad: 4 }),
            frameRate: 8
        });

        this.anims.create({
            key: 'idle',
            frames: this.anims.generateFrameNames('knight', { prefix: 'idle/frame', start: 0, end: 5, zeroPad: 4 }),
            frameRate: 8,
            repeat: -1
        });

        var lancelot = this.add.sprite(500, 536)

        lancelot.setOrigin(0.5, 1);
        lancelot.setScale(8);
        lancelot.play('idle');

        lancelot.on(Phaser.Animations.Events.ANIMATION_START, function (anim) {

            text.setText('Playing ' + anim.key);

        });

        this.input.on('pointerdown', function () {

            if (lancelot.anims.getName() === 'idle')
            {
                lancelot.playAfterRepeat('guardStart');
                lancelot.chain([ 'guard', 'guardEnd', 'idle' ]);
            }

        }, this);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#026bc6',
    pixelArt: true,
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка ассетов: подготовка сцены

Перед созданием анимаций необходимо загрузить все необходимые ресурсы. В методе preload мы используем this.load для загрузки атласа анимаций рыцаря, фонового изображения и тайлов для пола.

this.load.atlas('knight', 'assets/animations/knight.png', 'assets/animations/knight.json');
this.load.image('bg', 'assets/skies/clouds.png');
this.load.spritesheet('tiles', 'assets/tilemaps/tiles/fantasy-tiles.png', { frameWidth: 64, frameHeight: 64 });

Атлас 'knight' содержит все кадры анимаций в одном изображении и JSON-файл с их описанием. Это эффективно для памяти и производительности. Спрайтшит 'tiles' загружается с указанием размера одного кадра (64x64 пикселя), что позволяет Phaser корректно его нарезать.

Создание отдельных анимаций

Ядро системы — создание нескольких независимых анимаций через this.anims.create. Каждая анимация описывает одно действие персонажа.

this.anims.create({
    key: 'guardStart',
    frames: this.anims.generateFrameNames('knight', { prefix: 'guard_start/frame', start: 0, end: 3, zeroPad: 4 }),
    frameRate: 8
});

Метод generateFrameNames автоматически генерирует массив кадров из атласа, используя переданный префикс пути и диапазон номеров. Параметр zeroPad: 4 гарантирует, что имена кадров будут вида frame0000, frame0001 и т.д., что соответствует типичной структуре экспорта из редакторов анимаций. Анимация 'idle' установлена на бесконечное повторение (repeat: -1), чтобы персонаж стоял в ожидании действия.

Связывание анимаций в цепочку

Магия начинается при взаимодействии с игроком. По клику мыши мы запускаем последовательность. Ключевую роль играют два метода: playAfterRepeat и chain.

if (lancelot.anims.getName() === 'idle')
{
    lancelot.playAfterRepeat('guardStart');
    lancelot.chain([ 'guard', 'guardEnd', 'idle' ]);
}

Сначала проверяется текущая анимация через lancelot.anims.getName(). Если персонаж в состоянии 'idle', мы запускаем первую анимацию цепочки 'guardStart' с помощью playAfterRepeat. Этот метод гарантирует, что если текущая анимация ('idle') была на повторе, она завершит свой текущий цикл, и только потом начнется 'guardStart'. Это создает более естественный переход без резких обрывов.

Сразу после этого вызывается метод chain, который принимает массив с ключами следующих анимаций. Phaser автоматически воспроизведет 'guardStart', затем 'guard', 'guardEnd' и, наконец, вернется к 'idle'. Управление всей последовательностью передается движку.

Отслеживание событий анимации

Для отладки и создания реактивных интерфейсов полезно знать, какая анимация сейчас играет. В примере используется событие Phaser.Animations.Events.ANIMATION_START.

lancelot.on(Phaser.Animations.Events.ANIMATION_START, function (anim) {
    text.setText('Playing ' + anim.key);
});

Мы подписываем спрайт lancelot на событие начала новой анимации. Колбэк получает объект анимации anim, из которого можно извлечь её ключ (anim.key). Это позволяет, например, обновлять UI, логировать действия или синхронизировать звуковые эффекты с визуальными событиями.

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

Финальный шаг — создание экземпляра игры с конфигурацией. Важным параметром здесь является pixelArt: true.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    backgroundColor: '#026bc6',
    pixelArt: true,
    scene: Example
};
const game = new Phaser.Game(config);

Установка pixelArt: true включает специальный режим рендеринга, который отключает сглаживание пиксельных спрайтов при масштабировании. Это критически важно для сохранения четкого, ретро-стиля графики, когда спрайт рыцаря масштабируется в 8 раз (setScale(8)). Без этого флага изображение стало бы размытым.

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

Цепочки анимаций в Phaser 3 — это элегантное решение для создания сложного поведения персонажей без нагромождения кода. Движок сам управляет последовательностью, позволяя разработчику сосредоточиться на дизайне анимаций и логике игры. Для экспериментов попробуйте

  1. Добавить звуковые эффекты для каждой анимации в цепочке, используя событие ANIMATION_START
  2. Создать более длинные и разветвленные цепочки с условиями (например, если игрок кликнет снова, прервать цепь и запустить анимацию атаки)
  3. Использовать chain для неигровых объектов, например, для плавного затухания и появления UI-элементов