О чем этот пример
Движущиеся платформы — классический элемент платформеров, оживляющий игровой процесс. В этом примере показана мощная, но простая техника `setDirectControl()`, которая позволяет физическим телам платформ двигаться независимо от физического мира, а игроку — реалистично взаимодействовать с ними. Вы научитесь создавать платформы, управляемые твинами, и настраивать корректные столкновения, что станет основой для проектирования сложных и интересных уровней.
Версия 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.spritesheet('dude', 'src/games/firstgame/assets/dude.png', { frameWidth: 32, frameHeight: 48 });
this.load.atlas('tiles', 'assets/sets/platformer.png', 'assets/sets/platformer.json');
this.load.image('bg', 'assets/sets/background.png');
}
create ()
{
this.add.image(400, 300, 'bg');
const water = this.physics.add.staticGroup();
for (let i = 0; i < 6; i++)
{
water.create(i * 128, 552, 'tiles', '17');
}
const ground = this.physics.add.staticGroup();
ground.create(64, 536, 'tiles', '6');
ground.create(64, 536-128, 'tiles', '6');
ground.create(64, 536-256, 'tiles', '6');
ground.create(64, 536-384, 'tiles', '3');
ground.create(736, 536, 'tiles', '1');
this.add.image(740, 440, 'tiles', 'sign2');
const platform1 = this.physics.add.image(600, 128, 'tiles', 'platform1').setScale(0.5).setDirectControl().setImmovable();
const platform2 = this.physics.add.image(700, 270, 'tiles', 'platform1').setScale(0.5).setDirectControl().setImmovable();
const platform3 = this.physics.add.image(200, 400, 'tiles', 'platform1').setScale(0.5).setDirectControl().setImmovable();
this.physics.world.setBounds(0, -400, 800, 1000);
this.player = this.physics.add.sprite(64, 64, 'dude').setBounce(0.2).setCollideWorldBounds(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.tweens.add({
targets: platform1,
x: 200,
duration: 4000,
yoyo: true,
repeat: -1
});
this.tweens.add({
targets: platform2,
x: 250,
duration: 3000,
yoyo: true,
repeat: -1
});
this.tweens.add({
targets: platform3,
x: 550,
y: 450,
duration: 2500,
yoyo: true,
repeat: -1
});
this.cursors = this.input.keyboard.createCursorKeys();
this.physics.add.collider(this.player, ground);
this.physics.add.collider(this.player, [ platform1, platform2, platform3 ]);
this.physics.add.collider(this.player, water, () => this.player.setPosition(64, 64));
}
update ()
{
if (this.cursors.left.isDown)
{
this.player.setVelocityX(-160);
this.player.anims.play('left', true);
}
else if (this.cursors.right.isDown)
{
this.player.setVelocityX(160);
this.player.anims.play('right', true);
}
else
{
this.player.setVelocityX(0);
this.player.anims.play('turn');
}
if (this.cursors.up.isDown && this.player.body.touching.down)
{
this.player.setVelocityY(-330);
}
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
physics: {
default: 'arcade',
arcade: {
gravity: { y: 500 },
// debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и создание статического окружения
В методе preload загружаются необходимые ассеты: спрайтшит для персонажа, атлас тайлов для платформ и фоновое изображение.
В create первым делом добавляется фон. Затем создаются две статические физические группы (staticGroup) — water и ground. Статические тела оптимизированы для неподвижных объектов, например, земли.
const water = this.physics.add.staticGroup();
for (let i = 0; i < 6; i++)
{
water.create(i * 128, 552, 'tiles', '17');
}
Цикл создает ряд тайлов воды, выстраивая их в линию. При столкновении с водой игрока будет возвращать в начальную точку.
const ground = this.physics.add.staticGroup();
ground.create(64, 536, 'tiles', '6');
Таким же способом создаются статические платформы, формирующие рельеф уровня.
Создание динамических платформ под прямым управлением
Ключевой момент — создание платформ, которые могут двигаться. Для этого используется метод this.physics.add.image, который создает физическое тело с изображением.
const platform1 = this.physics.add.image(600, 128, 'tiles', 'platform1').setScale(0.5).setDirectControl().setImmovable();
Метод setDirectControl() переключает тело в режим прямого управления. Это означает, что его положение (координаты `x,y`) будет управляться напрямую (например, через твин), а не симуляцией физики Arcade. При этом тело все равно участвует в проверках столкновений.
Метод setImmovable(true) делает массу тела бесконечной. Это важно для движущихся платформ: при столкновении они не будут реагировать на импульс от игрока и не сдвинутся с заданной траектории.
Анимация движения с помощью твинов
Для задания движения платформам используется система твинов Phaser. Твин плавно изменяет свойства цели (targets) за указанное время (duration).
this.tweens.add({
targets: platform1,
x: 200,
duration: 4000,
yoyo: true,
repeat: -1
});
Здесь твин перемещает platform1 по оси X от начальной позиции 600 до 200 за 4 секунды. Параметры yoyo: true и repeat: -1 заставляют платформу двигаться туда-обратно по этому пути бесконечно.
Третий твин для platform3 демонстрирует одновременное изменение двух свойств — и `x, иy`, создавая диагональное движение.
Настройка физики, игрока и обработка столкновений
Границы физического мира расширяются вверх, чтобы игрок не упал за пределы экрана при высоком прыжке.
this.physics.world.setBounds(0, -400, 800, 1000);
Создается спрайт игрока с физическим телом. Метод setCollideWorldBounds(true) включает столкновение с границами мира.
Далее создаются три анимации для персонажа: движение влево, поворот (покой) и движение вправо. Ключевые кадры берутся из спрайтшита.
Регистрируются обработчики столкновений с помощью this.physics.add.collider.
this.physics.add.collider(this.player, ground);
this.physics.add.collider(this.player, [ platform1, platform2, platform3 ]);
this.physics.add.collider(this.player, water, () => this.player.setPosition(64, 64));
Первый и второй коллайдеры обеспечивают физическое взаимодействие игрока с землей и движущимися платформами. Третий коллайдер с водой принимает callback-функцию, которая мгновенно возвращает игрока в стартовую точку при касании воды.
Управление персонажем в игровом цикле
Вся логика управления вынесена в метод update, который вызывается на каждом кадре.
if (this.cursors.left.isDown)
{
this.player.setVelocityX(-160);
this.player.anims.play('left', true);
}
Код проверяет состояние клавиш-стрелок. Если нажата стрелка влево, телу игрока задается горизонтальная скорость (setVelocityX), и проигрывается соответствующая анимация.
if (this.cursors.up.isDown && this.player.body.touching.down)
{
this.player.setVelocityY(-330);
}
Прыжок возможен только при условии this.player.body.touching.down, что означает, что игрок стоит на какой-либо поверхности (земле или платформе). Это предотвращает прыжок в воздухе.
Что попробовать дальше
Метод setDirectControl() — это элегантное решение для создания движущихся платформ, лестниц или транспортеров в Arcade Physics. Он разделяет управление положением объекта и физические расчеты, позволяя легко анимировать их через твины или пользовательский код. Для экспериментов попробуйте: изменить траекторию платформ на круговую, добавить платформу, управляемую игроком, или создать "лифт", который активируется при нажатии на переключатель.
