О чем этот пример
Создание реалистичных траекторий движения для игровых объектов — ключевой навык в разработке игр. Встроенная физика часто ограничивает нас прямолинейным или параболическим движением, но многие игровые механики требуют более сложных и плавных путей, например, для полёта вражеского корабля, траектории заклинания или движения камеры. В этой статье мы разберём пример из официальной коллекции Phaser, который демонстрирует, как заставить физическое тело Matter.js плавно следовать по заранее построенной кривой (сплайну), игнорируя гравитацию и силы трения. Этот подход даёт полный контроль над траекторией, сохраняя при этом все преимущества физического движка для коллизий.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
tempLine = new Phaser.Geom.Line();
duration = 5000;
t = -1;
block;
curve;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('block', 'assets/sprites/flower-exo.png');
}
create ()
{
const graphics = this.add.graphics();
// let line = new Phaser.Geom.Line(Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500), Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500));
let line = new Phaser.Geom.Line(100, 500, 700, 100);
// graphics.fillStyle(0xff0000, 1);
// graphics.fillCircle(line.x1, line.y1, 8);
// graphics.fillStyle(0xff00ff, 1);
// graphics.fillCircle(line.x2, line.y2, 8);
let points = [];
points.push(line.getPointA());
const length = Phaser.Geom.Line.Length(line);
const waves = Math.ceil(length / 200);
let vx = 100;
let vy = 100;
let prevX = line.x1;
let prevY = line.y1;
for (let i = 1; i <= waves; i++)
{
let currentPoint = line.getPoint(i / waves);
// graphics.fillStyle(0xffff00).fillCircle(currentPoint.x, currentPoint.y, 4);
let ray = new Phaser.Geom.Line(prevX, prevY, currentPoint.x, currentPoint.y);
// graphics.lineStyle(1, 0xffffff).strokeLineShape(ray);
let normal = Phaser.Geom.Line.GetNormal(ray);
let midPoint = Phaser.Geom.Line.GetMidPoint(ray);
// graphics.fillStyle(0x00ff00).fillCircle(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy, 4);
points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));
prevX = currentPoint.x;
prevY = currentPoint.y;
vx *= -1;
vy *= -1;
}
points.push(line.getPointB());
this.curve = new Phaser.Curves.Spline(points);
graphics.lineStyle(1, 0xffffff, 1);
this.curve.draw(graphics, 64);
this.block = this.matter.add.image(line.x1, line.y1, 'block');
this.block.setFriction(0);
this.block.setFrictionAir(0);
this.block.setBounce(0);
this.input.once('pointerdown', () =>
{
this.t = 0;
}, this);
}
update (time, delta)
{
if (this.t === -1)
{
return;
}
this.t += delta;
if (this.t >= this.duration)
{
// Reached the end
this.block.setVelocity(0, 0);
}
else
{
const d = (this.t / this.duration);
const p = this.curve.getPoint(d);
this.block.setPosition(p.x, p.y);
}
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#1b1464',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
debug: true,
gravity: {
x: 0,
y: 0
}
}
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и построение базовой линии
Вся логика примера содержится в классе сцены, который наследуется от Phaser.Scene. В начале определяются свойства класса: временная линия для геометрических операций, общая длительность движения, счётчик времени и ссылки на сам объект и кривую.
В методе preload загружается спрайт для нашего движущегося объекта.
В create начинается основная работа. Сначала создаётся графический объект graphics для отладки и визуализации. Затем определяется прямая линия от точки (100, 500) до (700, 100). Эта линия будет служить основным направляющим вектором для нашей будущей извилистой траектории.
let line = new Phaser.Geom.Line(100, 500, 700, 100);
Генерация волнообразного пути (сплайна)
Цель — превратить прямую линию в плавную волнообразную кривую. Для этого мы создадим массив контрольных точек для сплайна.
Сначала в массив points добавляется начальная точка линии (Point A). Затем линия делится на несколько сегментов, количество которых (waves) зависит от её длины. Чем длиннее линия, тем больше "волн" будет у траектории.
В цикле для каждого сегмента вычисляется его середина (midPoint) и нормаль (normal) — перпендикулярный вектор к отрезку линии. Добавляя к середине отрезка нормаль, умноженную на переменные vx и vy, мы получаем точку, смещённую в сторону от прямой линии. На каждой итерации знаки vx и vy инвертируются, что создаёт характерный волнообразный зигзаг. Эти смещённые точки и становятся контрольными точками сплайна.
let normal = Phaser.Geom.Line.GetNormal(ray);
let midPoint = Phaser.Geom.Line.GetMidPoint(ray);
points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));
vx *= -1;
vy *= -1;
После цикла в массив добавляется конечная точка линии (Point B). Все собранные точки передаются конструктору Phaser.Curves.Spline, который строит по ним плавную кривую. Эту кривую можно отрисовать на graphics для наглядности.
Создание и настройка физического тела
Теперь создадим физический объект, который будет двигаться по построенной кривой. Используя менеджер физики Matter (this.matter), мы добавляем изображение в начальную точку линии.
Критически важный шаг — настройка физических свойств тела. Чтобы движение по кривой было чётким и не подвергалось влиянию симуляции, мы отключаем все мешающие силы:
- setFriction(0) убирает трение о другие тела.
- setFrictionAir(0) отключает сопротивление воздуха.
- setBounce(0) убирает упругость, чтобы тело не отскакивало от границ мира (хотя в данном примере гравитация тоже отключена).
this.block = this.matter.add.image(line.x1, line.y1, 'block');
this.block.setFriction(0);
this.block.setFrictionAir(0);
this.block.setBounce(0);
Движение начинается по клику мыши, что устанавливает счётчик времени this.t в 0, активируя логику в методе update.
Анимация движения в методе Update
Логика плавного перемещения реализована в методе update. Пока this.t равен -1 (ожидание клика), метод просто завершается.
После старта this.t увеличивается на значение delta (время, прошедшее с прошлого кадра). Это обеспечивает плавную анимацию, не зависящую от частоты кадров.
Ключевая формула вычисляет прогресс движения `dкак отношение прошедшего времени к общей длительности. Значениеd` изменяется от 0 (начало пути) до 1 (конец пути).
Затем мы запрашиваем у объекта кривой this.curve точку, соответствующую этому прогрессу, с помощью метода getPoint(d). Полученные координаты p.x и p.y напрямую задаются как новая позиция физического тела через setPosition.
const d = (this.t / this.duration);
const p = this.curve.getPoint(d);
this.block.setPosition(p.x, p.y);
Когда время истекает (this.t >= this.duration), тело останавливается, и его скорость явно обнуляется. Прямое задание позиции в обход физического движка — это и есть основной "фокус", позволяющий совместить сложную траекторию и физическое тело.
Конфигурация игры и физики Matter
Для работы примера необходима правильная настройка игры. В конфигурационном объекте важно активировать физический плагин Matter.js и задать его параметры.
Параметр default: 'matter' указывает Phaser использовать Matter.js в качестве основной физической системы. Внутри блока matter можно включить отладку (debug: true), чтобы видеть хитбоксы тел, и, что самое важное, отключить гравитацию, установив её компоненты `xиy` в 0. Это необходимо, чтобы тело не начало падать вниз и двигалось исключительно по заданной нами траектории.
physics: {
default: 'matter',
matter: {
debug: true,
gravity: {
x: 0,
y: 0
}
}
}
Что попробовать дальше
Этот пример наглядно демонстрирует мощный гибридный подход: мы используем физический движок Matter.js для коллизий и взаимодействий, но при этом берём управление позицией тела в свои руки, чтобы реализовать сложную, заранее определённую траекторию. Это открывает двери для создания разнообразных игровых механик.
**Идеи для экспериментов:**
1. Подставьте в код другую кривую, например, Phaser.Curves.Ellipse или Phaser.Curves.Path, чтобы создать круговые или многоугольные траектории.
2. Измените логику в update: попробуйте задавать не позицию, а скорость (setVelocity) в направлении следующей точки на кривой, чтобы получить более "физичное", но всё же управляемое движение.
3. Добавьте несколько тел, которые начинают движение по одной кривой с задержкой, создавая "змейку" из объектов.
4. Включите гравитацию и наблюдайте, как тело, движущееся по кривой, будет на неё реагировать при столкновении с другими физическими объектами.
