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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('backdrop', 'assets/pics/platformer-backdrop.png');
        this.load.image('cannon_head', 'assets/tests/timer/cannon_head.png');
        this.load.image('cannon_body', 'assets/tests/timer/cannon_body.png');
        this.load.spritesheet('chick', 'assets/sprites/chick.png', { frameWidth: 16, frameHeight: 18 });
    }

    create ()
    {
        this.anims.create({ key: 'fly', frames: this.anims.generateFrameNumbers('chick', [ 0, 1, 2, 3 ]), frameRate: 5, repeat: -1 });

        this.add.image(320, 256, 'backdrop').setScale(2);

        const cannonHead = this.add.image(130, 416, 'cannon_head').setDepth(1);
        const cannon = this.add.image(130, 464, 'cannon_body').setDepth(1);
        const chick = this.physics.add.sprite(cannon.x, cannon.y - 50, 'chick').setScale(2);
        const graphics = this.add.graphics({ lineStyle: { width: 10, color: 0xffdd00, alpha: 0.5 } });
        const line = new Phaser.Geom.Line();

        chick.disableBody(true, true);

        let angle = 0;

        this.input.on('pointermove', (pointer) =>
        {
            angle = Phaser.Math.Angle.BetweenPoints(cannon, pointer);
            cannonHead.rotation = angle;
            Phaser.Geom.Line.SetToAngle(line, cannon.x, cannon.y - 50, angle, 128);
            graphics.clear().strokeLineShape(line);
        });

        this.input.on('pointerup', () =>
        {
            chick.enableBody(true, cannon.x, cannon.y - 50, true, true);
            chick.play('fly');
            this.physics.velocityFromRotation(angle, 600, chick.body.velocity);
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 640,
    height: 512,
    parent: 'phaser-example',
    pixelArt: true,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 300 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

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

В методе preload мы загружаем все необходимые изображения. Обратите внимание на load.spritesheet для спрайта цыплёнка: мы загружаем лист с несколькими кадрами, чтобы позже создать анимацию.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('backdrop', 'assets/pics/platformer-backdrop.png');
this.load.image('cannon_head', 'assets/tests/timer/cannon_head.png');
this.load.image('cannon_body', 'assets/tests/timer/cannon_body.png');
this.load.spritesheet('chick', 'assets/sprites/chick.png', { frameWidth: 16, frameHeight: 18 });

В create первым делом создаётся анимация полёта из кадров спрайтшита. Затем на сцену добавляются статичные изображения фона и частей пушки. Важный момент: ствол пушки (cannonHead) получает метод .setDepth(1), чтобы он отрисовывался поверх тела пушки.

this.anims.create({ key: 'fly', frames: this.anims.generateFrameNumbers('chick', [ 0, 1, 2, 3 ]), frameRate: 5, repeat: -1 });
this.add.image(320, 256, 'backdrop').setScale(2);
const cannonHead = this.add.image(130, 416, 'cannon_head').setDepth(1);
const cannon = this.add.image(130, 464, 'cannon_body').setDepth(1);

Создание физического тела и визуализация прицела

Цыплёнок создаётся не как обычный спрайт, а как физическое тело Arcade с помощью this.physics.add.sprite. Это сразу добавляет ему свойство body для управления скоростью и столкновениями. Изначально тело отключается методом disableBody, чтобы цыплёнок не падал под действием гравитации до выстрела.

const chick = this.physics.add.sprite(cannon.x, cannon.y - 50, 'chick').setScale(2);
chick.disableBody(true, true);

Для наглядности мы создаём графический объект (graphics) и геометрическую линию (line), чтобы рисовать линию прицеливания от пушки до курсора.

const graphics = this.add.graphics({ lineStyle: { width: 10, color: 0xffdd00, alpha: 0.5 } });
const line = new Phaser.Geom.Line();

Поворот пушки и обновление линии прицела

Обработчик события pointermove обновляет угол и визуальные элементы в реальном времени при движении мыши.

1. Phaser.Math.Angle.BetweenPoints вычисляет угол (в радианах) между центром пушки и текущей позицией курсора. 2. Этот угол присваивается вращению ствола пушки (cannonHead.rotation). 3. Phaser.Geom.Line.SetToAngle перестраивает нашу геометрическую линию: задаёт её начало, угол и длину. 4. graphics.clear().strokeLineShape(line) стирает предыдущую линию и рисует новую.

this.input.on('pointermove', (pointer) => {
    angle = Phaser.Math.Angle.BetweenPoints(cannon, pointer);
    cannonHead.rotation = angle;
    Phaser.Geom.Line.SetToAngle(line, cannon.x, cannon.y - 50, angle, 128);
    graphics.clear().strokeLineShape(line);
});

Выстрел: преобразование угла в скорость

В момент клика (pointerup) происходит "выстрел".

1. Цыплёнок активируется в начальной позиции (у дула пушки) с помощью enableBody. 2. Запускается анимация его полёта. 3. Ключевой метод this.physics.velocityFromRotation принимает три аргумента: угол (в радианах), величину скорости (пикселей в секунду) и объект скорости тела (chick.body.velocity). Метод напрямую вычисляет и присваивает компоненты скорости velocity.x и velocity.y для движения под заданным углом.

this.input.on('pointerup', () => {
    chick.enableBody(true, cannon.x, cannon.y - 50, true, true);
    chick.play('fly');
    this.physics.velocityFromRotation(angle, 600, chick.body.velocity);
});

После этого физический движок Arcade берёт управление на себя: к телу цыплёнка применяется заданная начальная скорость и глобальная гравитация (gravity: { y: 300 }), в результате чего мы получаем красивую баллистическую траекторию.

Настройка физики в конфигурации игры

Чтобы всё работало, необходимо активировать физический плагин Arcade в конфигурации игры. Обратите внимание на параметр pixelArt: true, который включает сглаживание текстур для пиксельной графики.

const config = {
    type: Phaser.AUTO,
    width: 640,
    height: 512,
    parent: 'phaser-example',
    pixelArt: true,
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 300 }
        }
    },
    scene: Example
};

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

Использование physics.velocityFromRotation — это самый прямой и производительный способ задать движение физического тела по вектору в Phaser 3 Arcade. Этот метод идеально подходит для снарядов, прыжков под углом или запуска объектов с катапульты. **Идеи для экспериментов:** 1. Измените величину скорости (второй аргумент) в зависимости от времени удержания кнопки мыши, чтобы реализовать "силу" выстрела. 2. Добавьте несколько "снарядов" с созданием новых спрайтов при каждом выстреле и ограничением их количества на сцене. 3. Попробуйте применить velocityFromRotation не к скорости (velocity), а к ускорению (acceleration) тела, чтобы получить другой тип движения.