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

Анимация — это сердце любой живой 2D-игры. Умение не только создавать плавные циклы, но и точно управлять их воспроизведением — ключевой навык. В этом примере Phaser демонстрирует, как создать анимацию из спрайтшита, запустить её с возможностью паузы, перезапуска и отслеживания внутреннего состояния. Это полезно для отладки сложных анимационных сцен, синхронизации действий с анимацией и создания интерактивных демо.

Версия 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.spritesheet('mummy', 'assets/animations/mummy37x45.png', { frameWidth: 37, frameHeight: 45 });
    }

    create ()
    {
        //  Frame debug view
        this.frameView = this.add.graphics({ fillStyle: { color: 0xff00ff }, x: 32, y: 32 });

        //  Show the whole animation sheet
        this.add.image(32, 32, 'mummy', '__BASE').setOrigin(0);

        const config = {
            key: 'walk',
            frames: this.anims.generateFrameNumbers('mummy'),
            frameRate: 8,
            yoyo: true,
            repeat: -1
        };

        this.anim = this.anims.create(config);

        this.sprite = this.add.sprite(400, 300, 'mummy').setScale(4);

        this.sprite.anims.load('walk');

        //  Debug text
        this.progress = this.add.text(100, 500, 'Progress: 0%', { color: '#00ff00' });

        this.input.keyboard.on('keydown-SPACE', function (event) {

            this.sprite.anims.play('walk');

        }, this);

        this.input.keyboard.on('keydown-P', function (event) {

            if (this.sprite.anims.isPaused)
            {
                this.sprite.anims.resume();
            }
            else
            {
                this.sprite.anims.pause();
            }

        }, this);

        this.input.keyboard.on('keydown-R', function (event) {
            this.sprite.anims.restart();
        }, this);
    }

    update ()
    {
        this.updateFrameView();
        const debug = [
            'SPACE to start animation, P to pause/resume, R to restart',
            '',
            'Progress: ' + this.sprite.anims.getProgress() + '%',
            'Accumulator: ' + this.sprite.anims.accumulator,
            'NextTick: ' + this.sprite.anims.nextTick
        ];
        this.progress.setText(debug);
    }

    updateFrameView ()
    {
        this.frameView.clear();
        this.frameView.fillRect(this.sprite.frame.cutX, 0, 37, 45);
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов и подготовка

Всё начинается с метода preload. Здесь загружается спрайтшит — единое изображение, содержащее все кадры анимации. Важно правильно указать размер одного кадра.

this.load.spritesheet('mummy', 'assets/animations/mummy37x45.png', { frameWidth: 37, frameHeight: 45 });

После загрузки, в create, для наглядности отрисовывается весь спрайтшит с помощью метода this.add.image. Ключ '__BASE' указывает на исходное, неразрезанное изображение.

this.add.image(32, 32, 'mummy', '__BASE').setOrigin(0);

Создание и конфигурация анимации

Анимация в Phaser создаётся как объект конфигурации, который передаётся в менеджер анимаций сцены this.anims.create(config).

const config = {
    key: 'walk',
    frames: this.anims.generateFrameNumbers('mummy'),
    frameRate: 8,
    yoyo: true,
    repeat: -1
};
this.anim = this.anims.create(config);

- key: Уникальное имя анимации для последующего обращения. - frames: Массив кадров. generateFrameNumbers автоматически создаёт его из всех кадров загруженного спрайтшита 'mummy'. - frameRate: Скорость воспроизведения в кадрах в секунду. - yoyo: При true анимация проигрывается в прямом и обратном порядке. - repeat: Количество повторений. -1 означает бесконечный цикл.

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

Создаётся спрайт, который будет использовать анимацию. Анимация предварительно загружается в его собственный анимационный компонент.

this.sprite = this.add.sprite(400, 300, 'mummy').setScale(4);
this.sprite.anims.load('walk');

Управление привязано к клавиатуре. Обратите внимание на проверку isPaused для реализации переключателя паузы.

// Запуск
this.input.keyboard.on('keydown-SPACE', function (event) {
    this.sprite.anims.play('walk');
}, this);

// Пауза/возобновление
this.input.keyboard.on('keydown-P', function (event) {
    if (this.sprite.anims.isPaused) {
        this.sprite.anims.resume();
    } else {
        this.sprite.anims.pause();
    }
}, this);

// Перезапуск с начала
this.input.keyboard.on('keydown-R', function (event) {
    this.sprite.anims.restart();
}, this);

Отладка и визуализация в реальном времени

Метод update вызывается каждый кадр игры и идеально подходит для отладочной информации.

update() {
    this.updateFrameView();
    const debug = [
        'SPACE to start animation, P to pause/resume, R to restart',
        '',
        'Progress: ' + this.sprite.anims.getProgress() + '%',
        'Accumulator: ' + this.sprite.anims.accumulator,
        'NextTick: ' + this.sprite.anims.nextTick
    ];
    this.progress.setText(debug);
}

- getProgress(): Возвращает прогресс проигрывания текущего повтора от 0 до 1. - accumulator и nextTick: Внутренние тайминговые счетчики движка анимации. Их отслеживание помогает понять, как накапливается время между кадрами.

Отдельный метод updateFrameView визуализирует текущий отображаемый кадр на изображении всего спрайтшита.

updateFrameView() {
    this.frameView.clear();
    this.frameView.fillRect(this.sprite.frame.cutX, 0, 37, 45);
}

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

Этот пример даёт полный контроль над жизненным циклом анимации. Вы можете не только запускать и останавливать её, но и получать детальную служебную информацию. Для экспериментов попробуйте: изменить frameRate и yoyo на лету в ответ на события игры; привязать перезапуск анимации restart() к попаданию по врагу; использовать getProgress() для синхронизации звуковых эффектов или вызова логики в определённый момент анимации (например, в момент удара).