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

Движок Arcade Physics в Phaser предоставляет базовые и эффективные алгоритмы для обработки столкновений. Но что, если стандартное поведение «отталкивания» не подходит для вашей игровой механики? В этой статье мы разберем пример, где отключается встроенный физический расчет по вертикали и реализуется своя логика взаимодействия объектов. Это полезно для создания нестандартных симуляций, например, падающих блоков, которые не отскакивают, а «укладываются» друг на друга, или для любых сценариев, где нужно полное управление процессом коллизии.

Версия 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.image('crate', 'assets/sprites/crate32.png');
    }

    create ()
    {
        this.physics.world.checkCollision.up = false;

        const group = this.physics.add.group({
            bounceY: 0.5,
            collideWorldBounds: true,
            dragY: 30,
            frameQuantity: 18,
            key: 'crate',
            setXY: { x: 200, y: 0, stepX: 16, stepY: -64 },
            velocityY: 300
        });

        group.shuffle();

        for (const crate of group.getChildren())
        {
            crate.body.customSeparateY = true;
        }

        this.physics.add.collider(group, group, function (gameObject1, gameObject2)
        {
            const b1 = gameObject1.body;
            const b2 = gameObject2.body;

            if (b1.y > b2.y)
            {
                b2.y += (b1.top - b2.bottom);
                b2.stop();
            }
            else
            {
                b1.y += (b2.top - b1.bottom);
                b1.stop();
            }
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            fps: 60,
            gravity: { y: 600 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и отключение стандартной логики

В методе create сцены, прежде всего, отключается проверка коллизий с верхней границей мира. Это нужно, чтобы объекты, появившиеся сверху, не «залипали» у границы.

this.physics.world.checkCollision.up = false;

Далее создается физическая группа (Physics Group) — мощный инструмент для управления множеством однотипных объектов с физикой. В конфигурации задаются ключевые параметры: упругость по Y, столкновение с границами мира, сопротивление (drag) для плавного падения, количество спрайтов, их начальное положение и вертикальная скорость.

const group = this.physics.add.group({
    bounceY: 0.5,
    collideWorldBounds: true,
    dragY: 30,
    frameQuantity: 18,
    key: 'crate',
    setXY: { x: 200, y: 0, stepX: 16, stepY: -64 },
    velocityY: 300
});

Метод group.shuffle() случайным образом меняет порядок элементов в группе, что добавляет визуальной разнородности при их создании.

Включение кастомного разделения по вертикали

Самый важный шаг — указать движку, что для каждого тела в группе стандартный алгоритм разделения (separate) по оси Y должен быть заменен нашим. Это делается путем установки флага customSeparateY в true для тела каждого спрайта в группе.

for (const crate of group.getChildren())
{
    crate.body.customSeparateY = true;
}

После этой настройки Arcade Physics будет по-прежнему обнаруживать столкновения между объектами группы (так как collider добавлен), но не будет автоматически раздвигать их по вертикали. Вся ответственность за определение их конечной позиции ложится на нашу функцию-обработчик.

Логика обработчика коллизий

Между всеми объектами группы добавляется коллайдер. Его callback-функция получает два столкнувшихся игровых объекта. Внутри мы вручную рассчитываем, как они должны расположиться после столкновения.

this.physics.add.collider(group, group, function (gameObject1, gameObject2)
{
    const b1 = gameObject1.body;
    const b2 = gameObject2.body;

    if (b1.y > b2.y)
    {
        b2.y += (b1.top - b2.bottom);
        b2.stop();
    }
    else
    {
        b1.y += (b2.top - b1.bottom);
        b1.stop();
    }
});

Алгоритм прост: определяется, какой из объектов находится ниже (имеет большую координату `y). Затем верхний объект «поднимается» ровно настолько, чтобы его нижняя грань (bottom) коснулась верхней грани (top) нижнего объекта. Методbody.stop()` мгновенно обнуляет скорость тела, чтобы оно не проваливалось дальше под действием гравитации. В результате создается эффект аккуратной укладки ящиков друг на друга без отскока.

Настройка игры и физики

Конфигурация игры стандартна, но с важными параметрами физики. Устанавливается Arcade Physics как движок по умолчанию. Отладочный режим выключен (debug: false). Сильная гравитация (gravity: { y: 600 }) заставляет объекты быстро падать, что наглядно демонстрирует работу нашей кастомной логики укладки.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            fps: 60,
            gravity: { y: 600 }
        }
    },
    scene: Example
};

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

Использование customSeparateY открывает тонкую настройку физических взаимодействий, когда стандартного поведения Arcade Physics недостаточно. Вы получаете контроль над тем, что происходит в момент столкновения. Для экспериментов попробуйте изменить логику в обработчике: например, заставить объекты не останавливаться, а лишь замедляться, или реализовать «цепную реакцию» падения при достижении определенного веса. Можно также поэкспериментировать с флагом customSeparateX для создания нестандартного бокового взаимодействия, например, скользящего толчка.