О чем этот пример
Движение объектов по заданным траекториям — частая задача в играх: от патрулирования врагов до движения платформ. Встроенный в Phaser 3 Arcade Physics не имеет готового метода `followPath`, но его можно реализовать, комбинируя возможности модулей Curves и Physics. Эта статья покажет, как создать физическое тело, которое будет перемещаться по сложному пути, сохраняя все свойства физической модели, такие как коллизии и гравитация. Мы разберем пример, где один объект ведет другого, и вы сможете адаптировать этот подход для своих игровых механик.
Версия 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('block', 'assets/sprites/block.png');
this.load.image('lemming', 'assets/sprites/lemming.png');
}
create ()
{
// const path = this.createLoopPath();
const path = this.createZigZagPath();
const graphics = this.add.graphics({
fillStyle: { color: 0xffff00, alpha: 0.6 },
lineStyle: { width: 2, color: 0x0000ff, alpha: 0.6 }
});
const start = path.getStartPoint();
const distance = path.getLength();
const duration = 20000;
const speed = distance / duration;
const speedSec = 1000 * speed;
const tSpeed = 1 / duration;
const tSpeedSec = 1000 * tSpeed;
let t = 0;
console.log('distance (px)', distance);
console.log('time (ms)', duration);
console.log('speed (px/ms)', speed);
console.log('speed (px/s)', speedSec);
console.log('speed (t/ms)', tSpeed);
console.log('speed (t/s)', tSpeedSec);
const block = this.physics.add.image(start.x, start.y, 'block')
.setImmovable(true)
.setAlpha(0.5);
const lemming = this.physics.add.image(block.x, block.y - 128, 'lemming')
.setGravityY(30000);
this.physics.add.collider(block, lemming);
this.physics.world.on('worldstep', (delta) =>
{
t += delta * tSpeedSec;
if (t > 1)
{
t -= 1;
block.body.reset(start.x, start.y);
graphics.clear();
path.draw(graphics);
}
path.getTangent(t, block.body.velocity);
block.body.velocity.scale(speedSec);
block.setRotation(block.body.velocity.angle());
graphics.fillPointShape(block.body.center, 2);
});
}
createLoopPath ()
{
const path = new Phaser.Curves.Path(50, 500);
path.splineTo([ 164, 446, 274, 542, 412, 457, 522, 541, 664, 464 ]);
path.lineTo(700, 300);
path.lineTo(600, 350);
path.ellipseTo(200, 100, 100, 250, false, 0);
path.cubicBezierTo(222, 119, 308, 107, 208, 368);
path.ellipseTo(60, 60, 0, 360, true);
return path;
}
createZigZagPath ()
{
const path = new Phaser.Curves.Path(100, -50);
path.lineTo(100, 50);
const max = 8;
const h = 500 / max;
for (let i = 0; i < max; i++)
{
if (i % 2 === 0)
{
path.lineTo(700, 50 + h * (i + 1));
}
else
{
path.lineTo(100, 50 + h * (i + 1));
}
}
path.lineTo(100, 650);
return path;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
debug: false,
fps: 60,
timeScale: 1
}
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание пути
В методе preload загружаются два спрайта. В create создается объект пути (Path). В примере закомментирован вызов createLoopPath, который создает петлевидную трассу, но активен createZigZagPath, генерирующий зигзагообразный путь.
Класс Phaser.Curves.Path позволяет строить сложные траектории из отрезков (lineTo), сплайнов (splineTo), эллипсов (ellipseTo) и кривых Безье (cubicBezierTo). Путь задается в координатах игрового мира.
const path = this.createZigZagPath();
Для визуализации пути создается объект Graphics. Метод path.draw(graphics) отрисует линию пути на сцене.
const graphics = this.add.graphics({
fillStyle: { color: 0xffff00, alpha: 0.6 },
lineStyle: { width: 2, color: 0x0000ff, alpha: 0.6 }
});
path.draw(graphics);
Расчет скорости и параметра t
Ключевая концепция — параметризация пути. Параметр `tменяется от 0 (начало пути) до 1 (конец пути). Чтобы объект прошел весь путь за заданное время, нужно рассчитать, насколько быстро должен менятьсяt`.
Сначала получаем общую длину пути в пикселях и задаем желаемое время прохождения в миллисекундах.
const start = path.getStartPoint();
const distance = path.getLength();
const duration = 20000; // 20 секунд
Затем вычисляются скорости:
- speed — скорость в пикселях за миллисекунду.
- tSpeed — скорость изменения параметра `t` за миллисекунду.
Умножая эти значения на 1000, получаем скорости в секунду.
const speed = distance / duration;
const tSpeed = 1 / duration;
const tSpeedSec = 1000 * tSpeed; // Скорость изменения t в секунду
Эти расчеты гарантируют, что объект пройдет весь путь ровно за duration миллисекунд, независимо от длины и формы траектории.
Создание и связывание физических тел
Создаются два физических спрайта с помощью this.physics.add.image. Блок (block) будет следовать по пути, а лемминг (lemming) — падать на него под действием гравитации.
Блок делается неподвижным (setImmovable(true)), чтобы при коллизиях его не сдвигали другие тела. Леммингу задается большая гравитация по оси Y.
const block = this.physics.add.image(start.x, start.y, 'block')
.setImmovable(true)
.setAlpha(0.5);
const lemming = this.physics.add.image(block.x, block.y - 128, 'lemming')
.setGravityY(30000);
this.physics.add.collider(block, lemming);
Коллайдер обеспечивает физическое взаимодействие: лемминг будет приземляться на движущийся блок и ехать на нем, как на платформе.
Движение по пути в обработчике worldstep
Логика движения реализована внутри обработчика события worldstep. Это событие генерируется на каждом шаге физического движка (с частотой fps, указанной в конфиге). Параметр delta содержит время, прошедшее с предыдущего шага, в миллисекундах.
Внутри обработчика:
1. Увеличиваем параметр `tна величину, пропорциональную прошедшему времени (delta`).
2. Если `t` превысил 1 (конец пути), сбрасываем его и телепортируем блок в начало.
3. Метод path.getTangent(t, block.body.velocity) — это ключевой момент. Он вычисляет вектор касательной к пути в точке `tи записывает его вblock.body.velocity`. Этот вектор имеет единичную длину.
4. Масштабируем этот вектор на реальную скорость в пикселях в секунду (speedSec).
5. Поворачиваем блок (setRotation) в направлении вектора скорости.
this.physics.world.on('worldstep', (delta) =>
{
t += delta * tSpeedSec;
if (t > 1)
{
t -= 1;
block.body.reset(start.x, start.y);
graphics.clear();
path.draw(graphics);
}
path.getTangent(t, block.body.velocity);
block.body.velocity.scale(speedSec);
block.setRotation(block.body.velocity.angle());
graphics.fillPointShape(block.body.center, 2);
});
Последняя строка рисует маленькую точку в центре блока на каждом шаге, создавая след его движения.
Что попробовать дальше
Мы реализовали движение физического тела по произвольному пути, используя связку Curves.Path для задания траектории и события worldstep для обновления позиции. Этот подход сохраняет все преимущества Arcade Physics: коллизии, гравитацию и высокую производительность.
Идеи для экспериментов:
1. Замените путь на createLoopPath и посмотрите, как объект движется по сложной кривой.
2. Сделайте скорость блока переменной, изменяя speedSec в зависимости от `t` или по какому-либо событию.
3. Добавьте несколько независимых блоков, движущихся по одному или разным путям, и поместите на них динамических персонажей.
4. Используйте метод path.getPoint(t) для телепортации спрайта вместо плавного движения — это может пригодиться для мгновенных перемещений по рельсам.
