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

Когда в игре десятки одинаковых персонажей или врагов, запускающих одну и ту же анимацию синхронно, это выглядит неестественно и выдает шаблонность. Phaser предоставляет простой, но мощный инструмент `randomFrame` в методе `play()`, который позволяет каждому спрайту начать анимацию со случайного кадра. Это мгновенно добавляет разнообразие и живую, органичную динамику на сцену, экономя ресурсы на создание отдельных анимаций или сложной логики. В этой статье мы разберем конкретный пример, где три одинаковых мумии начинают "идти" с разных точек анимационного цикла, и посмотрим, как это сочетается с другими параметрами, такими как задержка (`delay`) и твины.

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

    create ()
    {
        this.add.image(400, 300, 'bg');

        this.anims.create({
            key: 'walk',
            frames: this.anims.generateFrameNumbers('mummy'),
            frameRate: 12,
            repeat: -1
        });

        const sprite1 = this.add.sprite(50, 100, 'mummy').setScale(4);
        const sprite2 = this.add.sprite(50, 300, 'mummy').setScale(4);
        const sprite3 = this.add.sprite(50, 500, 'mummy').setScale(4);

        //  By setting randomFrame to `true` it will pick a random frame to *START* the animation from

        sprite1.play({ key: 'walk', randomFrame: true, delay: 2000, showBeforeDelay: true });
        sprite2.play({ key: 'walk', randomFrame: true, delay: 2000, showBeforeDelay: true });
        sprite3.play({ key: 'walk', randomFrame: true, delay: 2000, showBeforeDelay: true });

        this.tweens.add({
            targets: [ sprite1, sprite2, sprite3 ],
            x: 750,
            flipX: true,
            yoyo: true,
            repeat: -1,
            duration: 8000,
            delay: 2000
        });
    }
}

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

const game = new Phaser.Game(config);

Подготовка анимации: основа без изменений

Перед тем как использовать случайный старт, нужно создать саму анимацию. Это стандартный процесс в Phaser. В методе preload() загружается спрайтшит, а в create() определяется анимация с ключом 'walk'. Обратите внимание, что generateFrameNumbers('mummy') автоматически создаст массив кадров из всего спрайтшита, что удобно для пошаговых анимаций.

this.anims.create({
    key: 'walk',
    frames: this.anims.generateFrameNumbers('mummy'),
    frameRate: 12,
    repeat: -1
});

Анимация 'walk' будет проигрываться с частотой 12 кадров в секунду и зацикливаться бесконечно (repeat: -1). Параметр randomFrame здесь еще не используется — он применяется позже, при запуске на конкретном спрайте.

Запуск анимации с параметром randomFrame

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

sprite1.play({ key: 'walk', randomFrame: true, delay: 2000, showBeforeDelay: true });

Это делает мумий визуально несинхронизированными с самого начала, как если бы они шли в разном ритме. Параметр delay: 2000 добавляет задержку в 2 секунды перед началом циклического воспроизведения, а showBeforeDelay: true гарантирует, что выбранный случайный кадр будет отображаться на экране сразу, а не после задержки. Без этого флага спрайт был бы невидим первые 2 секунды.

Сочетание с движением: твины для завершения картины

Чтобы продемонстрировать эффект от randomFrame в действии, пример добавляет движение спрайтов с помощью системы твинов Phaser. Твин анимирует свойства `xиflipX` для всех трех мумий одновременно.

this.tweens.add({
    targets: [ sprite1, sprite2, sprite3 ],
    x: 750,
    flipX: true,
    yoyo: true,
    repeat: -1,
    duration: 8000,
    delay: 2000
});

Движение начинается с той же задержки в 2 секунды (delay: 2000), что и старт цикла анимации. Параметры yoyo: true и repeat: -1 заставляют мумий ходить туда-обратно бесконечно, а flipX: true автоматически разворачивает спрайт по горизонтали при изменении направления. Несмотря на одинаковое движение, разный стартовый кадр анимации создает иллюзию независимого поведения.

Почему это работает: техническая сторона

Параметр randomFrame влияет только на начальный кадр анимации. Сама последовательность кадров, ее скорость и порядок воспроизведения не меняются. После старта анимация идет как обычно, с первого кадра последовательности? Нет. Phaser запоминает выбранный случайный кадр как новую точку отсчета для этого конкретного экземпляра анимации.

Это важно понимать: если анимация имеет 10 кадров, и она стартовала с кадра 7, то далее она проиграет кадры 8, 9, 10, 1, 2... и так по циклу. Это не "перемешивание" кадров, а просто сдвиг начальной точки. Такой подход идеально подходит для циклических анимаций (ходьба, полет, мигание), где нужно избежать монотонности, не нарушая логику цикла.

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

Использование randomFrame: true — это минималистичный и эффективный способ добавить разнообразия в сцену с множеством однотипных анимированных объектов. Он не требует дополнительных ресурсов или сложного кода. Для экспериментов попробуйте применить этот параметр к анимациям частиц, фоновых элементов или интерфейсных индикаторов. Например, можно сделать так, чтобы костры в таверне или мигающие огни на панели управления начинали работать с разной фазы, создавая более живое и убедительное окружение для игрока.