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

В классическом платформере игрок должен отталкиваться от земли, но не цепляться за потолки и стены. Простое добавление коллайдера между персонажем и платформой часто приводит к "залипанию" при прыжке вверх. В этой статье разберём, как использовать callback-функцию `process` в `collider`, чтобы разрешать столкновения только при движении вниз, создавая естественную физику для 2D-платформера. Этот приём позволит вам точно контролировать, когда объекты должны сталкиваться, а когда — игнорировать друг друга, открывая путь к более сложным игровым механикам.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    cursors;
    player;

    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.createAnims();

        this.add.image(400, 300, 'sky');

        this.platforms = this.physics.add.staticGroup();

        this.platforms.create(400, 568, 'ground').setScale(2).refreshBody();

        this.platforms.create(400, 400, 'ground').setScale(2, 1).refreshBody();

        this.player = this.physics.add.sprite(100, 450, 'dude');

        this.player.setBounce(0.2);
        this.player.setCollideWorldBounds(true);

        this.cursors = this.input.keyboard.createCursorKeys();

        this.stars = this.physics.add.group({
            allowGravity: false,
            bounceX: 1,
            bounceY: 1,
            collideWorldBounds: true,
            velocityY: -100
        });

        this.stars.createMultiple([
            { key: 'star', quantity: 5, setXY: { x: 200, y: 300, stepX: 100 } },
            { key: 'star', quantity: 5, setXY: { x: 200, y: 500, stepX: 100 } }
        ]);

        this.physics.add.collider(
            this.player,
            this.platforms,
            null,
            (player, platform) =>
            {
                return player.body.velocity.y >= 0;
            });

        this.physics.add.collider(this.stars, this.platforms);

        this.physics.add.overlap(this.player, this.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);
        }
    }

    createAnims ()
    {
        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
        });
    }

    collectStar (player, star)
    {
        star.disableBody(true, true);
    }
}

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

Проблема простого коллайдера

Если создать обычный коллайдер между персонажем и статичной платформой, физический движок Arcade будет обрабатывать все столкновения. Это означает, что при прыжке вверх персонаж ударится головой о нижнюю часть платформы и упадёт обратно, что выглядит неестественно для большинства платформеров. Нам нужно, чтобы персонаж мог проходить сквозь платформу снизу и приземлялся на неё сверху.

Именно эту проблему решает четвёртый параметр метода this.physics.add.collider() — callback-функция process. Она позволяет задать условие, при котором столкновение должно быть обработано.

Коллайдер с callback-функцией process

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

this.physics.add.collider(
    this.player,
    this.platforms,
    null,
    (player, platform) =>
    {
        return player.body.velocity.y >= 0;
    });

Здесь this.player — это спрайт персонажа, this.platforms — статическая группа платформ. Третий параметр (null) — это функция, которая вызывается уже *после* случившегося столкновения. А четвёртый параметр — наша функция process. Она вызывается для каждой пары объектов *перед* проверкой физикой столкновения между ними.

Функция получает два аргумента: player и platform. Возвращаемое значение true или false определяет судьбу столкновения в этом кадре. Если функция вернёт true — движок проверит столкновение и обработает его. Если false — столкновение будет проигнорировано, и объекты пройдут друг сквозь друга.

Логика условия: только при падении

Внутри функции process проверяется простая, но мощная логика.

return player.body.velocity.y >= 0;

Выражение player.body.velocity.y — это вертикальная скорость тела персонажа. В системе координат Phaser (и большинства 2D-движков) ось Y направлена вниз. Поэтому: - velocity.y > 0 означает, что персонаж движется **вниз** (падает). - velocity.y < 0 означает, что персонаж движется **вверх** (прыгает). - velocity.y = 0 означает, что персонаж находится в состоянии покоя по вертикали.

Условие player.body.velocity.y >= 0 возвращает true только в двух случаях: когда персонаж стоит на месте или когда он падает. Именно в эти моменты мы *разрешаем* столкновение с платформой. Если же персонаж прыгает вверх (velocity.y < 0), условие вернёт false, и столкновение с нижней частью платформы проигнорируется. Это создаёт поведение "полупроницаемой" платформы, характерное для игр вроде Mario.

Контекст: остальная настройка физики

Чтобы callback process работал корректно, важна общая физическая настройка сцены. Она задаётся в конфигурации игры.

const config = {
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 300 },
            debug: false
        }
    },
    scene: Example
};

Гравитация (gravity: { y: 300 }) постоянно придаёт персонажу положительную скорость по оси Y (толкает вниз). Это гарантирует, что после прыжка (velocity.y станет отрицательной) персонаж рано или поздно начнёт падать, его velocity.y станет положительной, и условие в process разрешит столкновение для мягкого приземления.

Также обратите внимание на проверку в методе update(), которая позволяет прыгать только при касании земли:

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

Свойство this.player.body.touching.down будет true, только если в текущем кадре физика зафиксировала касание нижней частью тела персонажа другого коллайдера. Благодаря нашему умному коллайдеру, это касание возникает, только когда персонаж стоит или падает на платформу.

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

Использование callback-функции process в this.physics.add.collider() — это ключ к созданию продвинутой и управляемой физики в Phaser. Вы можете реализовать не только полупроницаемые платформы, но и, например, платформы-убийцы (столкновение только при падении с большой скоростью), временные препятствия или условные проходы, активируемые ключами. Для экспериментов попробуйте изменить условие: разрешите столкновение только при определённой горизонтальной скорости или при наличии у персонажа特殊ного статуса (например, 'защищён').