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

Одновременная анимация множества одинаковых объектов часто выглядит неестественно и механистически. В реальных играх взрывы, появления врагов или магические эффекты должны запускаться с небольшими временными смещениями, создавая ощущение хаотичности и живости. В этой статье мы разберем технику добавления случайных задержек для анимаций в 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('boom', 'assets/sprites/explosion.png', { frameWidth: 64, frameHeight: 64, endFrame: 23 });
    }

    create ()
    {
        const config = {
            key: 'explode',
            frames: 'boom',
            frameRate: 30,
            repeat: -1,
            repeatDelay: 2000
        };

        this.anims.create(config);

        for (let i = 0; i < 256; i++)
        {
            let x = Phaser.Math.Between(0, 800);
            let y = Phaser.Math.Between(0, 600);

            let boom = this.add.sprite(x, y, 'boom', 23);

            //  Each one can have a random start delay
            boom.play({
                key: 'explode',
                delay: Math.random() * 3000
            });
        }
    }
}

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

const game = new Phaser.Game(config);

Подготовка спрайтшита и создание анимации

Перед созданием анимаций необходимо загрузить спрайтшит — изображение, содержащее все кадры одной анимации. В методе preload() мы указываем путь к файлу и его параметры.

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

Здесь 'boom' — это ключ ассета, frameWidth и frameHeight задают размер одного кадра, а endFrame определяет последний кадр в последовательности (всего 24 кадра, с 0 по 23).

Далее в create() мы создаем саму анимацию, используя конфигурационный объект. Обратите внимание на параметр repeatDelay.

const config = {
    key: 'explode',
    frames: 'boom',
    frameRate: 30,
    repeat: -1,
    repeatDelay: 2000
};
this.anims.create(config);

key — уникальное имя анимации. frames указывает на ключ загруженного спрайтшита. frameRate задает скорость воспроизведения (30 кадров в секунду). repeat: -1 означает бесконечное повторение. repeatDelay: 2000 — это пауза в 2000 миллисекунд (2 секунды) между циклами анимации. После проигрывания всех кадров анимация замрет на 2 секунды, прежде чем начаться заново.

Массовое создание спрайтов

Чтобы продемонстрировать технику, мы создаем 256 спрайтов, случайно разбросанных по сцене. Для генерации случайных координат используется встроенный генератор Phaser.Math.Between.

for (let i = 0; i < 256; i++)
{
    let x = Phaser.Math.Between(0, 800);
    let y = Phaser.Math.Between(0, 600);
    let boom = this.add.sprite(x, y, 'boom', 23);
}

Каждый спрайт создается методом this.add.sprite. Последний аргумент 23 — это индекс стартового кадра. Мы используем последний кадр спрайтшита (как бы "спокойное" состояние после взрыва), чтобы до начала анимации все спрайты отображались одинаково.

Ключевой прием: случайная задержка старта

Секрет оживления сцены кроется в методе play(). Вместо простого вызова boom.play('explode') мы передаем ему объект с настройками, где и задаем задержку.

boom.play({
    key: 'explode',
    delay: Math.random() * 3000
});

Параметр delay принимает значение в миллисекундах. Math.random() * 3000 генерирует случайное число от 0 до 3000 (3 секунд). Это означает, что каждый из 256 спрайтов начнет свою анимацию "взрыва" в случайный момент в течение первых 3 секунд после запуска сцены. Из-за repeatDelay в 2 секунды и случайного старта анимации никогда не синхронизируются, создавая непрерывный "рваный" эффект.

Как работает полный цикл

Давайте проследим жизненный цикл одного спрайта: 1. **Создание:** Он появляется на сцене с кадром 23 ("тишина после взрыва"). 2. **Ожидание:** Спрайт ждет случайное время (от 0 до 3 секунд), заданное параметром delay. 3. **Воспроизведение:** Запускается анимация explode. Она проигрывает 24 кадра со скоростью 30 FPS (длительность ~0.8 секунды). 4. **Пауза:** Срабатывает repeatDelay — спрайт снова замирает на кадре 23 на 2 секунды. 5. **Повтор:** Цикл (шаги 3-4) повторяется бесконечно (repeat: -1).

Поскольку задержка старта (delay) применяется только при первом запуске, а repeatDelay — между каждым повтором, анимации всех объектов постепенно рассинхронизируются, создавая иллюзию независимости.

Вариации и практическое применение

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

**Разные диапазоны задержки:** Для врагов, появляющихся волнами, можно задать фиксированную задержку для первой группы и увеличивать ее для последующих.

delay: 1000 + (i * 200) // Первый через 1 сек, каждый следующий на 0.2 сек позже

**Комбинация с другими параметрами play():** Метод play() может принимать и другие настройки, например, startFrame или timeScale.

boom.play({
    key: 'explode',
    delay: Math.random() * 2000,
    timeScale: Phaser.Math.Between(0.5, 1.5) // Случайная скорость анимации
});

**Типичные сценарии использования:** * **Взрывы и фейерверки:** Как в примере. * **Фоновая анимация среды:** Мерцание звезд, покачивание растений, капли дождя. * **Появление врагов или ресурсов:** Чтобы они возникали не строго по строчке, а "набегающей волной". * **Эффекты UI:** Постепенное появление элементов интерфейса.

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

Использование случайной или прогрессирующей задержки (delay) в методе play() — это простой, но мощный способ добавить реалистичности и визуального разнообразия в вашу игру. Это разрывает механическую синхронность, делая групповые анимации живыми и непредсказуемыми. Для экспериментов попробуйте: 1. Заменить Math.random() на шум Перлина для более плавного, "природного" распределения задержек. 2. Связать значение delay с расстоянием спрайта от определенной точки на сцене, создавая "волну" анимации. 3. Управлять задержками динамически, например, останавливать и снова запускать все анимации с новыми случайными задержками по нажатию клавиши.