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

При создании динамичных игр важно понимать, как движутся физические тела от кадра к кадру. Встроенные методы `body.deltaXFinal()` и `body.deltaYFinal()` в Phaser 3 позволяют получить точное смещение тела с учётом всех сил — гравитации, скорости, трения и столкновений. Эта статья покажет, как визуализировать эти векторы, чтобы отлаживать физику и создавать более предсказуемое поведение объектов. На примере с леммингами, платформами и шипастыми шарами мы разберём, как рассчитать и отрисовать итоговое смещение тела за кадр. Этот приём полезен не только для визуальной отладки, но и для реализации сложной логики, зависящей от точного перемещения объектов.

Версия 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('platform', 'assets/sprites/platform.png');
        this.load.image('lemming', 'assets/sprites/lemming.png');
        this.load.image('spikedball', 'assets/sprites/spikedball.png');
    }

    create ()
    {
        const platforms = this.physics.add.group({
            key: 'platform',
            frameQuantity: 3,
            setXY: { x: 400, y: 150, stepY: 150 },
            velocityX: 60,
            immovable: true
        });

        const [ platform1, platform2, platform3 ] = platforms.getChildren();

        platform1.setFrictionX(1);
        platform2.setFrictionX(0.5);
        platform3.setFrictionX(0);

        const lemmings = this.physics.add.group({ gravityY: 600 });

        lemmings.createMultiple([
            {
                key: 'lemming',
                repeat: 3,
                setXY: { x: 250, y: 0, stepX: 100 }
            },
            {
                key: 'lemming',
                repeat: 3,
                setXY: { x: 250, y: 200, stepX: 100 }
            },
            {
                key: 'lemming',
                repeat: 3,
                setXY: { x: 250, y: 350, stepX: 100 }
            }
        ]);

        this.physics.add.group({
            key: 'spikedball',
            frameQuantity: 6,
            setXY: { x: 0, y: 625, stepX: 150 },
            angularVelocity: 60
        });

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

        const graphics = this.add.graphics({ lineStyle: { color: 0xffff00 } });

        this.events.on('postupdate', () =>
        {
            graphics.clear();

            const bodies = Array.from(this.physics.world.bodies);
        for (const body of bodies)
            {
                const { x, y } = body.center;

                graphics.lineBetween(x, y, x + 100 * body.deltaXFinal(), y + 100 * body.deltaYFinal());
            }
        });
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и создание физических групп

В методе preload загружаются три спрайта: платформа, лемминг и шипастый шар. Основная работа происходит в create. Здесь создаются три ключевые физические группы.

Группа платформ настраивается с начальной горизонтальной скоростью (velocityX: 60) и свойством immovable: true, что делает их статичными при столкновениях. Три платформы размещаются вертикально с шагом в 150 пикселей.

const platforms = this.physics.add.group({
    key: 'platform',
    frameQuantity: 3,
    setXY: { x: 400, y: 150, stepY: 150 },
    velocityX: 60,
    immovable: true
});

Группа леммингов создаётся с вертикальной гравитацией (gravityY: 600). Лемминги размещаются тремя рядами с помощью createMultiple.

const lemmings = this.physics.add.group({ gravityY: 600 });
lemmings.createMultiple([...]);

Группа шипастых шаров добавляется в нижнюю часть экрана с постоянной угловой скоростью (angularVelocity: 60).

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

Каждой платформе задаётся разный коэффициент трения по оси X с помощью метода setFrictionX(). Это напрямую влияет на итоговое смещение объектов, которые с ней сталкиваются.

platform1.setFrictionX(1);
platform2.setFrictionX(0.5);
platform3.setFrictionX(0);

Значение `1— максимальное трение (объект почти не скользит),0` — его отсутствие. Это демонстрируется на леммингах: на нижней платформе они будут двигаться вместе с ней, а на верхних — отставать или соскальзывать.

Коллайдер регистрирует столкновения между группой леммингов и группой платформ.

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

Визуализация финального смещения (deltaFinal)

Самый важный элемент примера — визуализация векторов финального смещения тел. Создаётся объект Graphics для рисования линий.

const graphics = this.add.graphics({ lineStyle: { color: 0xffff00 } });

Затем, на событие postupdate (которое срабатывает после всех физических расчётов), вешается обработчик. В нём сначала очищается холст (graphics.clear()), а затем для каждого физического тела в мире (this.physics.world.bodies) рисуется линия.

this.events.on('postupdate', () => {
    graphics.clear();
    const bodies = Array.from(this.physics.world.bodies);
    for (const body of bodies) {
        const { x, y } = body.center;
        graphics.lineBetween(x, y, x + 100 * body.deltaXFinal(), y + 100 * body.deltaYFinal());
    }
});

Методы body.deltaXFinal() и body.deltaYFinal() возвращают смещение тела за последний шаг физики с учётом всех корректировок от столкновений. Мы умножаем эти значения на 100 для наглядности визуализации. Линия показывает направление и относительную величину этого финального смещения.

Что показывают векторы на практике

В запущенном примере можно наблюдать: 1. **У платформ** векторы направлены строго вправо и имеют постоянную длину, так как их движение задано только скоростью velocityX, а трение и гравитация на них не действуют. 2. **У леммингов** векторы сложнее. На них действует гравитация, поэтому у падающих леммингов векторы направлены вниз. При приземлении на движущуюся платформу вектор становится горизонтальным, но его длина различается в зависимости от трения платформы. 3. **У шипастых шаров** векторы очень короткие или нулевые, так как они лишь вращаются на месте (angularVelocity), но не имеют линейного перемещения в пространстве.

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

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

Использование deltaXFinal() и deltaYFinal() открывает тонкий контроль над движением. Вы можете использовать эти значения не только для отладки, но и в игровой логике — например, для определения реального перемещения персонажа за кадр, расчёта силы удара или создания следов. **Идеи для экспериментов:** 1. Измените гравитацию леммингов или скорость платформ и понаблюдайте за изменением векторов. 2. Попробуйте применить силу (setVelocity) к леммингам и посмотрите, как вектор смещения мгновенно меняет направление. 3. Используйте длину вектора (рассчитанную через Math.hypot) для включения визуальных или звуковых эффектов только при значительном перемещении объекта.