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

Создание платформ, которые движутся по заданным траекториям, — это мощный приём для оживления игрового мира. В этой статье мы разберём пример из официальных демонстраций Phaser, где платформы не просто статичны, а двигаются, создавая динамичные препятствия для игрока. Вы узнаете, как использовать методы `setDirectControl()` и `setImmovable()` в связке с твинерами для создания сложных движущихся элементов, с которыми персонаж может сталкиваться и взаимодействовать, как с обычными землёй или стенами.

Версия 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);

Создание движущихся платформ с физикой

Ключевая особенность примера — это создание платформ, которые являются физическими телами, но при этом управляются напрямую через их координаты (`x,y`), а не через импульсы или скорость. Это позволяет анимировать их движение с помощью системы твинов Phaser, сохраняя при этом все свойства коллизии.

В методе create() создаются три такие платформы. Обратите внимание на цепочку методов, применяемых к каждому спрайту.

const platform1 = this.physics.add.image(600, 128, 'tiles', 'platform1').setScale(0.5).setDirectControl().setImmovable();

Метод setDirectControl() указывает физическому движку Arcade, что положение этого тела будет управляться вручную (например, через твин или прямое присваивание `x`/`y), и движок должен корректно обрабатывать его коллизии в этом случае. МетодsetImmovable()` делает тело неподвижным при столкновениях — это важно, чтобы движущаяся платформа не отталкивалась от игрока, а, наоборот, сама толкала его.

Анимация движения через твины

Для задания траектории движения платформ используется встроенная система твинов Phaser. Твин плавно изменяет свойства цели (targets) по заданным правим.

this.tweens.add({
    targets: platform1,
    x: 200,
    duration: 4000,
    yoyo: true,
    repeat: -1
});

Этот код заставляет platform1 двигаться от изначальной координаты x=600 до x=200 за 4 секунды. Параметр yoyo: true означает, что после достижения конечной точки анимация проиграется в обратном порядке. repeat: -1 заставляет анимацию повторяться бесконечно. Аналогичные твины заданы для других платформ, создавая разнообразные и непредсказуемые для игрока траектории.

Настройка коллизий и реакция на столкновения

Чтобы игрок взаимодействовал с платформами и окружающим миром, необходимо настроить коллайдеры. В примере используется несколько типов взаимодействий.

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));

Первые два коллайдера обеспечивают стандартное физическое столкновение: игрок не может проходить сквозь статичную землю (ground) и движущиеся платформы. Третий коллайдер с водой (water) добавляет реакцию на столкновение — колбэк-функцию. При касании воды игрок телепортируется в начальную точку (64, 64), имитируя "смерть" или сброс уровня. Важно, что движущиеся платформы передаются в коллайдер как массив, что удобно для группировки.

Управление персонажем и проверка опоры

Логика управления персонажем находится в методе update(). Она обрабатывает нажатия клавиш стрелок для движения и прыжка.

if (this.cursors.up.isDown && this.player.body.touching.down)
{
    this.player.setVelocityY(-330);
}

Особенность прыжка — проверка this.player.body.touching.down. Это свойство физического тела становится true, когда тело соприкасается с чем-либо снизу. Это предотвращает "двойной прыжок" в воздухе и позволяет прыгать только тогда, когда персонаж стоит на земле или, что важно для нашего примера, на одной из движущихся платформ. Благодаря корректно настроенным коллизиям, платформы с setDirectControl() воспринимаются системой физики как твёрдая поверхность.

Что попробовать дальше

Использование setDirectControl() и твинов — это эффективный способ создания динамических уровней в платформерах. Персонаж может уверенно стоять на движущихся платформах, а вы получаете полный контроль над их траекториями. Для экспериментов попробуйте: изменить твины на движение по кругу или сложным сплайнам; сделать платформы, которые разрушаются при касании; или создать лифт, который активируется только когда игрок на него запрыгнул.