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

Реализация реалистичного управления автомобилем — частый запрос в играх с видом сверху. В этом примере используется движок Matter.js в Phaser для создания машины, которая не просто поворачивает спрайт, а физически поворачивает корпус, применяя силу к его краям. Этот подход даёт более правдоподобное и "тяжелое" поведение транспортного средства, идеально подходящее для гоночных или приключенческих игр. Статья разберёт ключевые техники: создание физического тела из изображения, использование векторов для расчёта направления, применение силы в конкретных точках тела для имитации поворота и отслеживание этих точек для отладки. Вы научитесь управлять автомобилем, который ведёт себя как настоящий объект с массой и инерцией.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    vec = Phaser.Physics.Matter.Matter.Vector;
    cursors;
    tracker2;
    tracker1;
    car;

    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.add.tileSprite(400, 300, 800, 600, 'soil');

        this.car = this.matter.add.image(400, 300, 'car');
        this.car.setAngle(-90);
        this.car.setFrictionAir(0.2);
        this.car.setMass(10);

        this.matter.world.setBounds(0, 0, 800, 600);

        this.tracker1 = this.add.rectangle(0, 0, 4, 4, 0x00ff00);
        this.tracker2 = this.add.rectangle(0, 0, 4, 4, 0xff0000);

        this.cursors = this.input.keyboard.createCursorKeys();
    }

    update ()
    {
        const point1 = this.car.getTopRight();
        const point2 = this.car.getBottomRight();

        this.tracker1.setPosition(point1.x, point1.y);
        this.tracker2.setPosition(point2.x, point2.y);
        
        const speed = 0.03;
        const angle = this.vec.angle(point1, point2);
        const force = {x: Math.cos(angle) * speed, y: Math.sin(angle) * speed};
        if (this.cursors.up.isDown)
        {
            this.car.thrust(0.05);
            this.steer(this.vec.neg(force));
        }
        else if (this.cursors.down.isDown)
        {
            this.car.thrustBack(0.05);
            this.steer(force);
        }
    }

    steer (force)
    {
        if (this.cursors.left.isDown)
        {
            Phaser.Physics.Matter.Matter.Body.applyForce(this.car.body, this.car.getTopRight(), force);
        }
        else if (this.cursors.right.isDown)
        {
            Phaser.Physics.Matter.Matter.Body.applyForce(this.car.body, this.car.getBottomRight(), this.vec.neg(force));
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'matter',
        matter: {
            debug: false,
            gravity: {
                x: 0,
                y: 0
            }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Настройка сцены и создание автомобиля

В методе preload загружаются текстуры фона и спрайта машины. В create происходит основная инициализация.

Фон создаётся как тайловая спрайта для заполнения всего экрана. Ключевой объект — this.car — это не обычный спрайт, а Physics Image. Метод this.matter.add.image создаёт спрайт, который сразу же становится физическим телом движка Matter.

После создания автомобилю задаются начальные физические свойства:

this.car.setAngle(-90); // Поворачиваем машину на 90 градусов против часовой стрелки ("смотрит" вверх).
this.car.setFrictionAir(0.2); // Устанавливаем сопротивление воздуха, чтобы машина постепенно останавливалась.
this.car.setMass(10); // Задаём массу тела, влияющую на его инерцию.

Также здесь создаются два маленьких прямоугольника-трекера (tracker1 и tracker2). Они будут визуально отображать точки на корпусе машины, к которым применяется сила для поворота, что очень полезно для отладки.

Векторная математика и расчёт силы

Логика управления сосредоточена в методе update. Основная идея — вычислять вектор, направленный вдоль правой стороны машины, и использовать его для создания силы поворота.

Сначала получаем две точки на корпусе: правый верхний и правый нижний углы машины (относительно её текущего поворота).

const point1 = this.car.getTopRight();
const point2 = this.car.getBottomRight();

Позиции трекеров обновляются для наглядности. Затем вычисляется угол вектора, идущего от point1 к point2. Этот вектор всегда направлен вдоль правого борта автомобиля.

const angle = this.vec.angle(point1, point2);
const force = {x: Math.cos(angle) * speed, y: Math.sin(angle) * speed};

Переменная force — это нормализованный вектор силы, который будет применяться перпендикулярно направлению движения для создания крутящего момента. Его направление (this.vec.neg(force) или force) зависит от того, едем мы вперёд или назад.

Логика движения и поворота

Управление разделено на два этапа: движение вперёд/назад и поворот.

Движение осуществляется методами thrust (разгон вперёд) и thrustBack (разгон назад) физического тела this.car. Эти методы применяют силу прямо вдоль текущей оси тела.

if (this.cursors.up.isDown) {
    this.car.thrust(0.05); // Применить силу вперёд.
    this.steer(this.vec.neg(force)); // Вызвать поворот с инвертированным вектором силы.
}
else if (this.cursors.down.isDown) {
    this.car.thrustBack(0.05); // Применить силу назад.
    this.steer(force); // Вызвать поворот с обычным вектором силы.
}

Поворот реализован в отдельном методе steer. Здесь используется низкоуровневый API Matter.js — Phaser.Physics.Matter.Matter.Body.applyForce. Он применяет силу не к центру масс тела, а к конкретной точке, что и создаёт вращающий момент.

if (this.cursors.left.isDown) {
    // Применяем силу к правому верхнему углу, чтобы развернуть машину влево.
    Phaser.Physics.Matter.Matter.Body.applyForce(this.car.body, this.car.getTopRight(), force);
}
else if (this.cursors.right.isDown) {
    // Применяем противоположную силу к правому нижнему углу, чтобы развернуть вправо.
    Phaser.Physics.Matter.Matter.Body.applyForce(this.car.body, this.car.getBottomRight(), this.vec.neg(force));
}

Именно этот приём — применение разнонаправленных сил к разным краям корпуса — и создаёт реалистичный эффект поворота автомобиля с учётом его физической модели.

Конфигурация Matter.js и запуск игры

Важную роль играет корректная настройка физического движка в конфигурации игры. Для этого примера выбран Matter.js с отключённой гравитацией.

physics: {
    default: 'matter',
    matter: {
        debug: false, // Отключаем отладочную отрисовку collision bodies.
        gravity: {
            x: 0,
            y: 0 // Гравитация отключена, движение зависит только от прикладываемых сил.
        }
    }
}

Также в create вызывается this.matter.world.setBounds, чтобы установить границы мира, за которые физические тела не будут вылетать. Без этого машина могла бы уехать за пределы экрана. Эта конфигурация создаёт изолированную арену, идеальную для экспериментов с управлением.

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

Пример демонстрирует мощную комбинацию высокоуровневых методов Phaser (thrust) и низкоуровневого доступа к API Matter.js (applyForce) для создания продвинутой физической модели управления. Автомобиль обладает инерцией, реалистично входит в повороты и требует навыка для управления. **Идеи для экспериментов:** 1. Измените параметры setFrictionAir и setMass, чтобы почувствовать разницу в управлении между легковушкой и грузовиком. 2. Добавьте "дрифт", временно увеличивая силу поворота или уменьшая трение при зажатой отдельной клавише. 3. Создайте трассу с физическими статичными телами-стенами (используя this.matter.add.rectangle) и попробуйте проехать по ней, не врезаясь. 4. Включите режим отладки (debug: true), чтобы увидеть реальную форму коллайдера автомобиля.