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

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

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

Живой запуск

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

Исходный код


// Forward speed in px/s.
const SPEED = 100;

// Turning speed in deg/s.
// At 60 steps/s, this is 1.5 deg/step.
const ROTATION_SPEED = 90;

// The angle tolerance in degrees.
const TOLERANCE = 3;

class Example extends Phaser.Scene
{
    cursor;
    ship;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('space', 'assets/skies/space2.png');
        this.load.image('ship', 'assets/sprites/thrust_ship.png');
        this.load.image('cursor', 'assets/sprites/drawcursor.png');
    }

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

        this.ship = this.physics.add.image(200, 150, 'ship')
            .setBodySize(20, 20)
            .setVelocity(SPEED, 0);

        this.cursor = this.add.image(0, 0, 'cursor').setAlpha(0);

        this.add.text(10, 10, 'Click and hold to steer to target.');

        this.input.on('pointermove', (pointer) =>
        {
            this.cursor
                .setPosition(pointer.worldX, pointer.worldY)
                .setAlpha(0.5);
        });
    }

    update ()
    {
        const { isDown, worldX, worldY } = this.input.activePointer;

        if (isDown)
        {
            const angleToPointer = Phaser.Math.RadToDeg(
                Phaser.Math.Angle.Between(this.ship.x, this.ship.y, worldX, worldY)
            );

            const angleDelta = Phaser.Math.Angle.ShortestBetween(this.ship.body.rotation, angleToPointer);

            if (Phaser.Math.Fuzzy.Equal(angleDelta, 0, TOLERANCE))
            {
                this.ship.body.rotation = angleToPointer;
                this.ship.setAngularVelocity(0);
                this.ship.body.debugBodyColor = 0xff0000;
            }
            else
            {
                this.ship.setAngularVelocity(Math.sign(angleDelta) * ROTATION_SPEED);
                this.ship.body.debugBodyColor = 0xffff00;
            }

            this.cursor.setAlpha(1);
        }
        else
        {
            this.cursor.setAlpha(0.5);
        }

        this.physics.velocityFromRotation(
            Phaser.Math.DegToRad(this.ship.body.rotation),
            SPEED,
            this.ship.body.velocity
        );
    }
}

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

const game = new Phaser.Game(config);

Настройка сцены и объектов

В начале кода определяются ключевые константы для скорости движения, скорости вращения и угловой погрешности. Эти значения позволяют легко настраивать поведение корабля.

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

const SPEED = 100;
const ROTATION_SPEED = 90;
const TOLERANCE = 3;

// В методе create:
this.ship = this.physics.add.image(200, 150, 'ship')
    .setBodySize(20, 20)
    .setVelocity(SPEED, 0);

this.cursor = this.add.image(0, 0, 'cursor').setAlpha(0);

Важно отметить, что кораблю сразу задается начальная скорость (SPEED, 0), что заставляет его двигаться вправо. Размер физического тела (setBodySize) уменьшен для более точного взаимодействия.

Отслеживание позиции курсора

Чтобы корабль знал, куда поворачивать, необходимо постоянно отслеживать положение мыши. Для этого используется событие pointermove. Каждый раз при движении указателя невидимый спрайт cursor перемещается в мировые координаты мыши и становится полупрозрачным. Это дает визуальную обратную свядь.

this.input.on('pointermove', (pointer) =>
{
    this.cursor
        .setPosition(pointer.worldX, pointer.worldY)
        .setAlpha(0.5);
});

Координаты берутся из pointer.worldX и pointer.worldY, что гарантирует корректную работу даже если камера перемещается.

Ядро логики: расчет угла и вращение

Вся основная механика реализована в методе update. Сначала проверяется, нажата ли кнопка мыши.

Если кнопка нажата, вычисляется угол от корабля к курсору с помощью Phaser.Math.Angle.Between. Этот метод возвращает угол в радианах, который затем преобразуется в градусы.

const angleToPointer = Phaser.Math.RadToDeg(
    Phaser.Math.Angle.Between(this.ship.x, this.ship.y, worldX, worldY)
);

Затем определяется кратчайшая разница между текущим вращением тела корабля (this.ship.body.rotation) и целевым углом. Для этого используется Phaser.Math.Angle.ShortestBetween, которая возвращает значение от -180 до 180 градусов, указывая направление для самого быстрого поворота.

const angleDelta = Phaser.Math.Angle.ShortestBetween(this.ship.body.rotation, angleToPointer);

Умное вращение с допуском

Просто поворачивать корабль к цели — не лучшая идея. Он будет бесконечно дрыгаться, пытаясь попасть в точный угол. Чтобы избежать этого, используется допуск (TOLERANCE) и метод Phaser.Math.Fuzzy.Equal.

Если разница в угле (angleDelta) меньше допуска, считается, что корабль уже смотрит на цель. Его вращение жестко устанавливается на целевой угол, угловая скорость обнуляется, а цвет отладочного тела меняется на красный.

if (Phaser.Math.Fuzzy.Equal(angleDelta, 0, TOLERANCE))
{
    this.ship.body.rotation = angleToPointer;
    this.ship.setAngularVelocity(0);
    this.ship.body.debugBodyColor = 0xff0000;
}

Если разница больше допуска, кораблю задается угловая скорость (setAngularVelocity). Направление вращения (по или против часовой стрелки) определяется знаком angleDelta, а величина — константой ROTATION_SPEED. Цвет тела становится желтым.

else
{
    this.ship.setAngularVelocity(Math.sign(angleDelta) * ROTATION_SPEED);
    this.ship.body.debugBodyColor = 0xffff00;
}

Движение вперед по направлению взгляда

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

Этот ключевой метод рассчитывает вектор скорости на основе заданного угла и величины. Угол берется из текущего вращения тела корабля (предварительно переведенного обратно в радианы), а величина — из константы SPEED. Рассчитанный вектор записывается прямо в свойство velocity физического тела корабля.

this.physics.velocityFromRotation(
    Phaser.Math.DegToRad(this.ship.body.rotation),
    SPEED,
    this.ship.body.velocity
);

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

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

Разобранный пример демонстрирует элегантное сочетание математики Phaser и физики Arcade для создания плавного и отзывчивого управления. Механика готова к использованию в играх жанра top-down или в космических симуляторах. Для экспериментов попробуйте: изменить SPEED и ROTATION_SPEED для создания разных типов кораблей (например, тяжелый и неповоротливый); добавить ускорение и трение вместо постоянной скорости; реализовать движение к цели не по клику, а автоматически, если она попадает в определенный радиус.