О чем этот пример
Часто в платформерах или аркадных играх нужны движущиеся мишени или собираемые предметы, которые перемещаются не хаотично, а по заданному пути. В этом примере мы рассмотрим, как заставить звёзды летать по эллиптическим траекториям с помощью кастомизированного класса спрайта и кривых Phaser. Этот подход даёт вам полный контроль над движением, скоростью и формой пути любого объекта, оживляя игровой мир.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
// this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
this.load.image('sky', 'src/games/firstgame/assets/sky.png');
this.load.image('ground', 'src/games/firstgame/assets/platform.png');
this.load.image('star', 'src/games/firstgame/assets/star.png');
this.load.spritesheet('dude', 'src/games/firstgame/assets/dude.png', {
frameWidth: 32,
frameHeight: 48
});
}
create ()
{
this.add.image(400, 300, 'sky');
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
platforms.create(600, 400, 'ground');
platforms.create(50, 250, 'ground');
this.player = this.physics.add.sprite(100, 450, 'dude');
this.player.setBounce(0.2);
this.player.setCollideWorldBounds(true);
const stars = this.physics.add.group({ allowGravity: false });
// x, y = center of the path
// width, height = size of the elliptical path
// speed = speed the sprite moves along the path per frame
stars.add(new FlyingStar(this, 150, 100, 100, 100, 0.005), true);
stars.add(new FlyingStar(this, 500, 200, 40, 100, 0.005), true);
stars.add(new FlyingStar(this, 600, 200, 40, 100, -0.005), true);
stars.add(new FlyingStar(this, 700, 200, 40, 100, 0.01), true);
this.anims.create({
key: 'left',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1
});
this.anims.create({
key: 'turn',
frames: [ { key: 'dude', frame: 4 } ],
frameRate: 20
});
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('dude', { start: 5, end: 8 }),
frameRate: 10,
repeat: -1
});
this.cursors = this.input.keyboard.createCursorKeys();
this.physics.add.collider(this.player, platforms);
this.physics.add.overlap(this.player, stars, this.collectStar, null, this);
}
update ()
{
const { left, right, up } = this.cursors;
if (left.isDown)
{
this.player.setVelocityX(-160);
this.player.anims.play('left', true);
}
else if (right.isDown)
{
this.player.setVelocityX(160);
this.player.anims.play('right', true);
}
else
{
this.player.setVelocityX(0);
this.player.anims.play('turn');
}
if (up.isDown && this.player.body.touching.down)
{
this.player.setVelocityY(-330);
}
}
collectStar (player, star)
{
star.disableBody(true, true);
}
}
class FlyingStar extends Phaser.Physics.Arcade.Sprite
{
constructor (scene, x, y, width, height, speed)
{
super(scene, x, y, 'star');
// This is the path the sprite will follow
this.path = new Phaser.Curves.Ellipse(x, y, width, height);
this.pathIndex = 0;
this.pathSpeed = speed;
this.pathVector = new Phaser.Math.Vector2();
this.path.getPoint(0, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
}
preUpdate (time, delta)
{
super.preUpdate(time, delta);
this.path.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
this.pathIndex = Phaser.Math.Wrap(this.pathIndex + this.pathSpeed, 0, 1);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false
}
},
scene: Example
};
const game = new Phaser.Game(config);
Анализ исходного кода: сцена и базовые объекты
Пример представляет собой классическую сцену платформера с фоном, статичными платформами, игроком и звёздами. Ключевое отличие от стандартных примеров — звёзды не падают под действием гравитации, а движутся по эллиптическим орбитам.
Код загружает стандартные ресурсы: фон ('sky'), платформы ('ground'), звёзды ('star') и спрайтшит персонажа ('dude'). В методе create() создаётся физический мир и все основные объекты.
this.player = this.physics.add.sprite(100, 450, 'dude');
this.player.setBounce(0.2);
this.player.setCollideWorldBounds(true);
Группа звёзд создаётся с отключённой гравитацией, так как их движение будет управляться вручную.
const stars = this.physics.add.group({ allowGravity: false });
Для взаимодействия между игроком и звёздами используется метод overlap, который вызывает функцию collectStar при пересечении.
this.physics.add.overlap(this.player, stars, this.collectStar, null, this);
Создание кастомного класса FlyingStar
Сердце примера — пользовательский класс FlyingStar, который наследуется от Phaser.Physics.Arcade.Sprite. Его задача — управлять движением спрайта по заданной кривой.
В конструкторе класса создаётся объект эллиптической кривой Phaser.Curves.Ellipse. Его параметры:
- `x,y` — центр эллипса.
- width, height — размеры эллипса по осям.
- speed — скорость перемещения точки вдоль кривой за кадр (значение от 0 до 1).
Также инициализируются вспомогательные переменные для отслеживания текущей позиции на кривой (pathIndex) и вектора позиции (pathVector).
class FlyingStar extends Phaser.Physics.Arcade.Sprite
{
constructor (scene, x, y, width, height, speed)
{
super(scene, x, y, 'star');
this.path = new Phaser.Curves.Ellipse(x, y, width, height);
this.pathIndex = 0;
this.pathSpeed = speed;
this.pathVector = new Phaser.Math.Vector2();
this.path.getPoint(0, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
}
}
Ключевой метод preUpdate вызывается каждый кадр перед обновлением физики. В нём происходит:
1. Получение текущей точки на кривой по индексу pathIndex.
2. Установка позиции спрайта в эту точку.
3. Увеличение индекса на значение скорости pathSpeed с зацикливанием в диапазоне от 0 до 1 с помощью Phaser.Math.Wrap. Это обеспечивает бесконечное движение по замкнутой траектории.
preUpdate (time, delta)
{
super.preUpdate(time, delta);
this.path.getPoint(this.pathIndex, this.pathVector);
this.setPosition(this.pathVector.x, this.pathVector.y);
this.pathIndex = Phaser.Math.Wrap(this.pathIndex + this.pathSpeed, 0, 1);
}
Создание и добавление звёзд на сцену
В методе create() основной сцены создаются экземпляры FlyingStar с разными параметрами и добавляются в физическую группу stars. Конструктор FlyingStar принимает сцену, координаты центра, размеры эллипса и скорость.
stars.add(new FlyingStar(this, 150, 100, 100, 100, 0.005), true);
stars.add(new FlyingStar(this, 500, 200, 40, 100, 0.005), true);
stars.add(new FlyingStar(this, 600, 200, 40, 100, -0.005), true);
stars.add(new FlyingStar(this, 700, 200, 40, 100, 0.01), true);
Обратите внимание на параметры:
- Первая звезда: центр в (150, 100), эллипс 100x100 (круг), скорость 0.005.
- Вторая и третья звезды: одинаковые эллипсы 40x100, но скорость третьей отрицательная (-0.005), что заставит её двигаться по той же траектории, но в обратном направлении.
- Четвёртая звезда: скорость 0.01 — она движется в два раза быстрее первой.
Второй аргумент true в методе add указывает, что объект должен быть автоматически добавлен на дисплейный список и в систему обновления сцены.
Взаимодействие игрока и звёзд
Для обработки столкновения игрока со звёздами используется this.physics.add.overlap. При наложении тел игрока и звезды вызывается функция collectStar.
this.physics.add.overlap(this.player, stars, this.collectStar, null, this);
Функция collectStar просто отключает тело звезды и скрывает её спрайт с помощью disableBody(true, true). Первый true отключает тело, второй — скрывает игровой объект.
collectStar (player, star)
{
star.disableBody(true, true);
}
Это стандартный подход для сбора предметов в Arcade Physics.
Управление движением игрока и анимации
Управление персонажем и его анимации реализованы стандартно для платформера на Phaser в методе update(). Движение по горизонтали управляется клавишами стрелок, прыжок — по нажатию стрелки вверх, но только если персонаж касается земли (this.player.body.touching.down).
if (left.isDown)
{
this.player.setVelocityX(-160);
this.player.anims.play('left', true);
}
else if (right.isDown)
{
this.player.setVelocityX(160);
this.player.anims.play('right', true);
}
else
{
this.player.setVelocityX(0);
this.player.anims.play('turn');
}
if (up.isDown && this.player.body.touching.down)
{
this.player.setVelocityY(-330);
}
Анимации ('left', 'turn', 'right') создаются в create() с помощью this.anims.create на основе кадров спрайтшита 'dude'.
Что попробовать дальше
Использование кастомного класса спрайта с кривыми Phaser открывает мощные возможности для создания сложных паттернов движения. Вы можете заменить Curves.Ellipse на Curves.Path, Curves.Spline или Curves.Line для создания линейных, ломаных или сплайновых траекторий. Экспериментируйте: меняйте скорость pathSpeed динамически, вращайте эллипс, комбинируйте несколько кривых для составного пути или используйте path.getTangent для автоматического поворота спрайта вдоль пути. Это отличная основа для создания движущихся платформ, патрулирующих врагов или сложных систем коллекционирования.
