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

При создании анимаций в Phaser вы можете столкнуться с ситуацией, когда заданная вами длительность каждого кадра анимации игнорируется, и анимация проигрывается с равномерной скоростью. Это происходит из-за особенностей работы метода `generateFrameNames` и порядка создания анимации. В этой статье мы разберем, почему это происходит и как правильно задавать индивидуальную длительность для каждого кадра, чтобы получить полный контроль над временной шкалой ваших анимаций, что особенно важно для эффектов вроде взрывов или затуханий.

Версия 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/animations/aseprite/';

        // this.load.aseprite('paladin', 'paladin.png', 'paladin.json');
        this.load.spritesheet('boom', 'assets/sprites/explosion.png', { frameWidth: 64, frameHeight: 64, endFrame: 23 });
    }

    create ()
    {
        const durationArray = [ 10, 20, 30, 40 ]

        const frames = this.anims.generateFrameNames("boom", { start: 0, end: 23 })

        for (let i = 0; i < frames.length; i++)
        {
            frames[ i ].duration = 10 + (i * 10);
        }

        this.game.anims.create({ key: "test", frames: frames })
        const sprite = this.add.sprite(400, 300, "boom").play({ key: "test" }) // will run at 24 FPS, ignore durationArray
    }
}

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

const game = new Phaser.Game(config);

Проблема: Длительность кадров игнорируется

В примере разработчик хочет создать анимацию взрыва, где каждый следующий кадр отображается дольше предыдущего, создавая эффект замедления. Он загружает спрайтшит и использует this.anims.generateFrameNames для создания массива кадров.

Затем в цикле он пытается задать каждому кадру свою длительность (свойство .duration).

for (let i = 0; i < frames.length; i++)
{
    frames[ i ].duration = 10 + (i * 10);
}

Однако после создания анимации с помощью this.game.anims.create и ее воспроизведения через .play(), анимация проигрывается с постоянной скоростью (например, 24 кадра в секунду), полностью игнорируя заданные значения duration. Это и есть баг (issue #7070), с которым можно столкнуться.

Причина: Конфликт генерации и модификации

Корень проблемы в том, что метод generateFrameNames предназначен для генерации кадров по определенным правилам из данных атласа или листа. Внутренняя логика Phaser может переопределять или сбрасывать пользовательские свойства duration, заданные после генерации, в момент создания самой анимации (create).

Ключевой момент: модифицировать объекты кадров, полученные из generateFrameNames, **после** их генерации, но **до** передачи в anims.create, — ненадежно. Нужен другой подход, который явно задает длительность на этапе конфигурации анимации.

Решение: Явное создание массива кадров

Вместо использования generateFrameNames и последующей модификации, создадим массив кадров вручную, используя объекты Phaser.Types.Animations.AnimationFrame. Это дает полный контроль над каждым параметром.

Сначала получим общее количество кадров из конфигурации загрузки спрайтшита. В нашем примере endFrame равен 23, значит, кадры от 0 до 23 включительно.

create()
{
    // Создаем пустой массив для кадров
    const framesArray = [];

    // Количество кадров = endFrame + 1
    const totalFrames = 24;

    // Заполняем массив объектами кадров с заданной длительностью
    for (let i = 0; i < totalFrames; i++)
    {
        framesArray.push({
            key: 'boom',       // Ключ текстуры
            frame: i,          // Номер кадра в спрайтшите
            duration: 10 + (i * 10) // Индивидуальная длительность кадра
        });
    }

    // Создаем анимацию с нашим массивом
    this.anims.create({
        key: 'customExplosion',
        frames: framesArray,
        repeat: 0 // Без повтора
    });

    // Создаем спрайт и проигрываем анимацию
    const sprite = this.add.sprite(400, 300, 'boom').play('customExplosion');
}

Этот метод гарантирует, что длительность каждого кадра будет применена корректно, так как мы с самого начала передаем в create готовые объекты с нужными свойствами.

Альтернатива: Использование generateFrameNumbers с duration

Phaser также предоставляет метод generateFrameNumbers, который может быть более удобным для простых спрайтшитов. Его можно использовать, если передать параметр duration в его конфигурацию. Однако для сложных случаев с разной длительностью кадров ручное создание массива (как показано выше) является предпочтительным и самым надежным способом.

// Этот метод подходит, если все кадры имеют ОДИНАКОВУЮ длительность
const frames = this.anims.generateFrameNumbers('boom', {
    start: 0,
    end: 23,
    duration: 50 // Длительность будет применена ко ВСЕМ кадрам
});

Для нашей задачи с нарастающей длителькой этот метод не подходит, так как он задает одну длительность для всех кадров.

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

Чтобы задать индивидуальную длительность для кадров анимации в Phaser, избегайте модификации массива, полученного из generateFrameNames. Вместо этого создавайте массив объектов-кадров вручную, явно указывая свойства key, frame и duration для каждого элемента. Это надежный способ обойти известную проблему и получить полный контроль над временными параметрами анимации. **Идеи для экспериментов:** 1. Создайте анимацию, где длительность кадра зависит от его индекса по более сложной формуле (например, синусоидальной). 2. Реализуйте эффект "бумеранга" для анимации, где длительность кадров увеличивается до середины, а затем уменьшается. 3. Используйте разные спрайтшиты для одного и того же объекта, переключая key в объектах кадров.