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

Плавность анимации — ключевой фактор для игрового комфорта. В этой статье мы разберем, как в Phaser создавать независимые от частоты кадров движения объектов, используя delta time и хранение пользовательских данных. Это особенно полезно для создания фоновых элементов, врагов или декораций, которые должны двигаться с постоянной скоростью на любых устройствах, от мощных ПК до мобильных телефонов.

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

Живой запуск

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

Исходный код


class SceneF extends Phaser.Scene {

    constructor ()
    {
        super('SceneF');

        this.mines = [];
    }

    create ()
    {
        this.cameras.main.setViewport(0, 136, 1024, 465);

        for (let i = 0; i < 8; i++)
        {
            let x = Phaser.Math.Between(400, 1400);
            let y = Phaser.Math.Between(0, 460);

            let mine = this.add.sprite(x, y, 'mine').play('mine');

            mine.setData('vx', Phaser.Math.FloatBetween(0.08, 0.14));

            this.mines.push(mine);
        }
    }

    update (time, delta)
    {
        for (let i = 0; i < this.mines.length; i++)
        {
            let mine = this.mines[i];

            mine.x -= mine.getData('vx') * delta;

            if (mine.x <= -100)
            {
                mine.x = 1224;
                mine.y = Phaser.Math.Between(0, 460);
            }
        }
    }

}

Подготовка сцены и контейнера

В конструкторе сцены мы задаем её ключ и инициализируем массив для хранения создаваемых спрайтов. Это стандартный подход для организации кода в Phaser.

constructor ()
{
    super('SceneF');

    this.mines = [];
}

Создание объектов с данными

В методе create мы сначала устанавливаем область просмотра для камеры. Затем в цикле создаем несколько спрайтов, используя случайные координаты. Ключевой момент — сохранение скорости для каждого объекта через метод setData. Это встроенная в Phaser система для хранения произвольной информации прямо на игровом объекте.

create ()
{
    this.cameras.main.setViewport(0, 136, 1024, 465);

    for (let i = 0; i < 8; i++)
    {
        let x = Phaser.Math.Between(400, 1400);
        let y = Phaser.Math.Between(0, 460);

        let mine = this.add.sprite(x, y, 'mine').play('mine');

        mine.setData('vx', Phaser.Math.FloatBetween(0.08, 0.14));

        this.mines.push(mine);
    }
}

Независимое от FPS движение

Метод update вызывается на каждом кадре. Для плавного движения, которое не зависит от частоты кадров, мы умножаем скорость объекта на параметр delta. Это время в миллисекундах, прошедшее с предыдущего кадра. Если игра замедлится, delta увеличится, и объект пройдет большее расстояние за один вызов update, сохраняя общую визуальную скорость.

update (time, delta)
{
    for (let i = 0; i < this.mines.length; i++)
    {
        let mine = this.mines[i];

        mine.x -= mine.getData('vx') * delta;

Респаун объектов за пределами экрана

Когда объект полностью покинул видимую область слева, мы перемещаем его обратно за правую границу и задаем новую случайную позицию по вертикали. Это создает эффект бесконечного цикла движения.

if (mine.x <= -100)
        {
            mine.x = 1224;
            mine.y = Phaser.Math.Between(0, 460);
        }
    }
}

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

Использование delta time и setData/getData — это основа для создания предсказуемой и плавной игровой анимации. Для экспериментов попробуйте изменить логику респауна, чтобы объекты появлялись не сразу, а с задержкой, или добавьте вертикальное движение, также управляемое через данные объекта. Можно реализовать разные типы движения для разных групп объектов, храня в данных не только скорость, но и тип траектории.