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

Создание плавного и реалистичного управления транспортным средством — частая задача в аркадных гонках. В этом примере мы разберем, как использовать Arcade Physics для управления машиной с инерцией, ограничением скорости и зависимой от скорости поворачиваемостью. Вы научитесь применять угловое ускорение, рассчитывать вектор скорости на основе вращения и динамически ограничивать маневренность, что пригодится не только в гонках, но и в любых играх с физикой транспорта или летательных аппаратов.

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

Живой запуск

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

Исходный код


// velocityFromRotation() can be called like a plain function.
const VelocityFromRotation = Phaser.Physics.Arcade.ArcadePhysics.prototype.velocityFromRotation;

class Racecar extends Phaser.Physics.Arcade.Image
{
    throttle = 0;

    configure ()
    {
        this.angle = -90;

        this.body.angularDrag = 120;
        this.body.maxSpeed = 1024;

        this.body.setSize(64, 64, true);
    }

    update (delta, cursorKeys)
    {
        const { left, right, up, down } = cursorKeys;

        if (up.isDown)
        {
            this.throttle += 0.5 * delta;
        }
        else if (down.isDown)
        {
            this.throttle -= 0.5 * delta;
        }

        this.throttle = Phaser.Math.Clamp(this.throttle, -64, 1024);

        if (left.isDown)
        {
            this.body.setAngularAcceleration(-360);
        }
        else if (right.isDown)
        {
            this.body.setAngularAcceleration(360);
        }
        else
        {
            this.body.setAngularAcceleration(0);
        }

        VelocityFromRotation(this.rotation, this.throttle, this.body.velocity);

        this.body.maxAngular = Phaser.Math.Clamp(90 * this.body.speed / 1024, 0, 90);
    }
}

class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('soil', 'assets/textures/soil.png');
        this.load.image('car', 'assets/sprites/car-yellow.png');
    }

    create ()
    {
        this.ground = this.add.tileSprite(256, 256, 512, 512, 'soil').setScrollFactor(0, 0);

        this.car = new Racecar(this, 256, 512, 'car');
        this.add.existing(this.car);
        this.physics.add.existing(this.car);
        this.car.configure();

        this.cursorKeys = this.input.keyboard.createCursorKeys();

        this.cameras.main.startFollow(this.car);
    }

    update (time, delta)
    {
        const { scrollX, scrollY } = this.cameras.main;

        this.ground.setTilePosition(scrollX, scrollY);

        this.car.update(delta, this.cursorKeys);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 512,
    height: 512,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: { debug: false }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Основа класса Racecar: наследник Physics Image

Класс Racecar наследуется от Phaser.Physics.Arcade.Image, что сразу дает нам спрайт с физическим телом Arcade Physics.

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

const VelocityFromRotation = Phaser.Physics.Arcade.ArcadePhysics.prototype.velocityFromRotation;

В конструкторе класса задается начальный угол и свойство throttle (газ), которое будет хранить текущее значение скорости вперед/назад.

Настройка физики в методе configure()

Метод configure() инициализирует физические параметры машины после ее добавления в мир физики.

Угол устанавливается в -90 градусов, чтобы машина изначально смотрела вверх. angularDrag создает сопротивление при вращении, делая повороты более плавными и инерционными. maxSpeed жестко ограничивает максимальную скорость. Метод setSize уменьшает размер физического тела (хитбокса) относительно спрайта, что часто используется для более справедливой коллизии.

this.angle = -90;
this.body.angularDrag = 120;
this.body.maxSpeed = 1024;
this.body.setSize(64, 64, true);

Логика управления в update()

Сердце класса — метод update(). Он обрабатывает ввод с клавиш-стрелок и обновляет состояние машины.

Клавиши ВВЕРХ и ВНИЗ изменяют значение throttle с учетом delta времени для независимости от частоты кадров. Значение затем ограничивается функцией Phaser.Math.Clamp между -64 (задний ход) и 1024 (максимальный газ).

Клавиши ВЛЕВО и ВПРАВО задают угловое ускорение (setAngularAcceleration), заставляя машину вращаться. Когда клавиши отпущены, ускорение сбрасывается в ноль, но angularDrag постепенно остановит вращение.

if (up.isDown) { this.throttle += 0.5 * delta; }
if (down.isDown) { this.throttle -= 0.5 * delta; }
this.throttle = Phaser.Math.Clamp(this.throttle, -64, 1024);

if (left.isDown) { this.body.setAngularAcceleration(-360); }
else if (right.isDown) { this.body.setAngularAcceleration(360); }
else { this.body.setAngularAcceleration(0); }

Связь скорости, направления и поворачиваемости

Самая важная строка в update() преобразует значение газа (throttle) и текущий угол вращения в вектор линейной скорости.

Функция VelocityFromRotation принимает угол (this.rotation), величину скорости (this.throttle) и объект, в который нужно записать результат (this.body.velocity). Это физически корректный способ задания движения вперед по направлению взгляда объекта.

VelocityFromRotation(this.rotation, this.throttle, this.body.velocity);

Следующая строка делает управление более реалистичным: максимальная угловая скорость (maxAngular) зависит от текущей линейной скорости (this.body.speed). Чем быстрее едет машина, тем медленнее она может поворачивать. Это вычисляется как доля от 90 градусов в секунду и также ограничивается.

this.body.maxAngular = Phaser.Math.Clamp(90 * this.body.speed / 1024, 0, 90);

Интеграция в сцену и камера

В сцене Example создается экземпляр Racecar, добавляется в дисплейный список и в мир физики. Камера начинает следовать за машиной с помощью startFollow.

this.car = new Racecar(this, 256, 512, 'car');
this.add.existing(this.car);
this.physics.add.existing(this.car);
this.cameras.main.startFollow(this.car);

В update сцены вызывается update машины, передавая delta и объект с клавишами. Также здесь реализован простой параллакс для фона (soil): текстура тайлспрайта смещается относительно позиции камеры, создавая иллюзию движения.

const { scrollX, scrollY } = this.cameras.main;
this.ground.setTilePosition(scrollX, scrollY);
this.car.update(delta, this.cursorKeys);

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

Этот пример демонстрирует мощь Arcade Physics для создания satisfying-геймплея аркадного вождения. Ключевые приемы — использование velocityFromRotation для движения вперед, углового ускорения для поворотов и динамического ограничения maxAngular для реализма. Для экспериментов попробуйте: изменить значения angularDrag и maxSpeed для другого "веса" машины; добавить частицы или след от шин при дрифте; реализовать автоматическое выравнивание машины при отпускании газа или использовать другую форму хитбокса через setCircle.