Продолжаю описание внутреннего устройства шаблона 3D-игры с ходьбой по ленте Мёбиуса.

В прошлой статье рассмотрена генерация самой ленты Мёбиуса и расчет вектора локальной гравитации. Если есть гравитация, значит, есть прыжки и падения. Их и рассмотрим.

Большинство FPS-контроллеров упрощают механику прыжка до мгновенного «выстрела» игроком. Пример из популярного контроллера для Godot Engine:

func _physics_process(delta: float) -> void:

# здесь идет получение пользовательского ввода...        

            if is_on_floor():

                        if Input.is_action_just_pressed(&"jump"):

                                    velocity.y = jump_height

            else:

                        velocity.y -= gravity * delta

А вот код из контроллера для Unity:

                //Do Jump

                if (jump && _jumpTimeoutDelta <= 0.0f)

                {

                    _verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);

                }

Различие примененных формул не должно вводить в заблуждение: механика прыжка абсолютно одинакова. В C# версии стартовая скорость определяется как физически корректная функция от желаемой высоты в верхней точке. В версии на gdscript стартовая вертикальная скорость «и есть высота прыжка», что некорректно, но интуитивно ясно, к тому же может быть оправдано получившейся микрооптимизацией (ни одного квадратного корня).

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

Отсутствие сопротивления воздуха в игре заметить по ощущениям практически невозможно, слишком мало его влияние на динамику настоящих прыжков. В то же время энергичному прыжку вверх в реальном мире должно предшествовать вполне заметное «приседание» с последующим коротким разгоном вверх.

Реалистичное приседание не должно, в свою очередь, игнорировать ньютоновскую механику: инертность в движениях живого человека видна невооруженным глазом. То есть подготовительное движение игрока вниз должно состоять из этапов вертикального разгона и торможения.

Кривая Hглаз(t) в этом случае имеет S-образный вид, характерный для многих процессов реального мира (чтобы увидеть на графике латинскую S, достаточно заменить высоту глубиной, то есть величиной (-1) * Hглаз).
Кривая Hглаз(t) в этом случае имеет S-образный вид, характерный для многих процессов реального мира (чтобы увидеть на графике латинскую S, достаточно заменить высоту глубиной, то есть величиной (-1) * Hглаз).

Формализуем задачу для доработки игровой механики: по команде «jump» уровень глаз (камеры) должен быть смещен вниз на фиксированную отрицательную высоту в два этапа — с ускорением и замедлением до остановки в нижней точке, далее выполняется разгон вверх до поднятия на начальную высоту с достижением максимальной вертикальной скорости, последующий свободный полет вверх в условиях гравитации может работать по аналогии с представленными выше примерами.

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

При чём здесь PD-контроллер?

Для работы ног персонажа в прыжках и приземлениях есть близкая аналогия из техники. Подвеска автомобиля при наезде на неровности (то есть положительном и отрицательном изменении высоты) делает именно то, что должны сделать «ноги» персонажа. Автомобили не прыгают по команде, но для выполнения прыжка достаточно вдавить автомобиль вниз и отпустить из этого положения.

Подвеска автомобиля работает как механический PD-контроллер. Пружина создает усилие, пропорциональное удалению от нейтрального положения, то есть ошибке. Это слагаемое P. Амортизатор отвечает за слагаемое D, реагируя на скорость движения.

Рассмотрим код, реализующий PD-контроллер взаимодействия персонажа с поверхностью.

Для большей наглядности объяснений будем считать, что вся прыгучесть обусловлена свойствами внешней среды, то есть имеем твердое тело персонажа и грунт, обладающий одновременно упругостью и вязкостью. Невидимые ноги персонажа работали бы аналогичным образом.

В каждый момент времени к телу персонажа приложены следующие силы:

  • сила тяжести;

  • упругая сила грунта, пропорциональная погружению в него;

  • сопротивление среды, пропорциональное скорости движения.

Предоставленный сам себе бездействующий персонаж под действием силы тяжести погружается на некоторую глубину, где реакция грунта равна его весу. С точки зрения PD-контроллера это и есть положение, в котором ошибка равна нулю.

При отклонении от этого положения равновесие сил нарушается и равнодействующая сил тяжести и упругости стремится вернуть персонажа в равновесное состояние.

Теперь рассмотрим код (в проекте находится в классе WorldPhysics).

static public Vector3 GetTotalAccel(Vector3 position, Vector3 velocity)

    {

        float depth = GetDepth(position);

// прочие действия

        Vector3 dragAccel = velocity * dragCoef;

        Vector3 localGravityDir = GetLocalGravity(position); 

        if (depth > 0) return localGravityDir * (GROUND_ELASTIC_COEF * depth + GRAVITY_COEF) + dragAccel;

        return localGravityDir * GRAVITY_COEF + dragAccel;

    }

Сначала по текущей позиции определяется глубина depth, она же величина деформации. Далее определяется сила сопротивления dragAccel (для простоты принята масса персонажа 1 кг, соответственно значения сил можно считать значениями ускорений) с помощью скалярного коэффициента сопротивления dragCoef. Проверив условие погруженности персонажа в грунт (depth > 0), прикладываем к нему силы упругости, тяжести и сопротивления.

Единичный вектор localGravityDir дает направление местной вертикали (напомню, мы находимся на поверхности ленты Мёбиуса, и вертикальная ось может быть направлена вообще куда угодно). Имена скалярных констант GROUND_ELASTIC_COEF и GRAVITY_COEF дают исчерпывающую информацию о них.

Из трех приложенных сил только dragAccel может иметь ненулевую проекцию на местную горизонтальную плоскость, что используется в логике горизонтальной составляющей движения. Здесь же важно, что вектор скорости velocity и, соответственно, вектор dragAccel может иметь ненулевую вертикальную составляющую.

PD-контроллер готов, осталось рассмотреть код, вдавливающий автомобиль вниз для прыжка.

Он помещен в метод MoveUpdate() класса состояния персонажа WalkingState.

if (jump && WorldPhysics.GetDepth(character.GetTransform().position) > JUMP_DEPTH)

        {

            jump = false;      

        }      

        if (jump && WorldPhysics.GetDepth(character.GetTransform().position) < JUMP_DEPTH)

        {

            acceleration += WorldPhysics.GetLocalGravity(character.GetTransform().position) * JUMP_ACCEL_COEF;      

        }

        character.MoveBody(acceleration);

Здесь jump – признак выполнения прыжка. В первом блоке if выполняется проверка, не превышает ли текущая глубина погружения в грунт заданной величины JUMP_DEPTH. Это условие прекращения выполнения прыжка. А в чем состоит выполнение прыжка, определяет следующий блок.

Прыжок выполняется приложением к персонажу вертикальной силы, равной по модулю JUMP_ACCEL_COEF. Выражение WorldPhysics.GetLocalGravity(character.GetTransform().position) отвечает за местную вертикаль, как localGravityDir в коде WorldPhysics.GetTotalAccel().

Говоря простым языком, мы вдавливаем персонажа в грунт с постоянной силой, затем, на заданной глубине погружения отпускаем его. На этом собственно выполнение прыжка окончено, остальное упругий грунт (он же ноги, он же «подвеска автомобиля», он же PD-контроллер) сделает сам.

При правильном подборе значений констант все движения будут как и задумано плавными, а динамику прыжка можно заметно варьировать.

Комментарии (0)