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

Движущиеся платформы — классический элемент платформеров, оживляющий игровой процесс. В этом примере показана мощная, но простая техника `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. Он разделяет управление положением объекта и физические расчеты, позволяя легко анимировать их через твины или пользовательский код. Для экспериментов попробуйте: изменить траекторию платформ на круговую, добавить платформу, управляемую игроком, или создать "лифт", который активируется при нажатии на переключатель.