Написание лучших уроков Unity
Управляйте скоростью сферы твердого тела.
Поддержка вертикального движения с помощью прыжков.
Определить землю и ее угол.
Используйте ProBuilder для создания тестовых уровней.
хранилищелицензийPDF
Здравствуйте! Я Джаспер Флик из Catlike Coding .
Физика толкает сферу
Я делаю высококачественные учебные пособия, которые научат вас всему о C # и программировании шейдеров с помощью Unity. Мои учебники написаны, потому что я считаю, что текст является лучшим носителем для этой темы. Они также содержат короткие анимации, когда я думаю, что они добавляют ценность.
- Ссылка https://www.patreon.com/catlikecoding
- Ссылка https://catlikecoding.com/unity/tutorials/movement/physics/
Физика толкает сферу
Двигаться по склонам.
Это вторая часть серии руководств по управлению движением персонажа. На этот раз мы будем использовать физический движок для создания более реалистичного движения и поддержки более сложных сред.
Этот учебник сделан с Unity 2019.2.11f1. Он также использует пакет ProBuilder.
Rigidbody, Жесткое тело
В предыдущем уроке мы ограничивали нашу сферу, оставаясь внутри прямоугольной области. Имеет смысл явно запрограммировать такое ограничение, потому что это просто. Но если мы хотим, чтобы наша сфера перемещалась по сложной трехмерной среде, мы должны поддерживать взаимодействие с произвольной геометрией. Вместо того, чтобы реализовывать это самостоятельно, мы будем использовать существующий физический движок Unity, которым является NVIDIA PhysX.
Существует два основных способа управления персонажем в сочетании с физическим движком. Во-первых, это подход с твердым телом, который заключается в том, чтобы персонаж вел себя как обычный физический объект, косвенно управляя им, применяя силы или изменяя его скорость. Вторым является кинематический подход, который заключается в прямом управлении, когда запрашивается только физический движок для выполнения пользовательского обнаружения столкновений.
Rigidbody Component, Компонент твердого тела
Мы будем использовать первый подход для управления сферой, что означает, что мы должны добавить
Rigidbody
к ней компонент. Мы можем использовать конфигурацию по умолчанию для твердого тела.
Добавление компонента достаточно, чтобы превратить нашу сферу в физический объект, при условии, что он все еще имеет свой
SphereCollider
компонент. Теперь мы переходим к физическому движку для столкновений, поэтому удалите код области из Update
. Vector3 newPosition = transform.localPosition + displacement;
// if (newPosition.x <allowArea.xMin) {
// newPosition.x = allowArea.xMin;
// speed.x = -velocity.x * bounciness;
//}
//…
transform.localPosition = newPosition;
С нашими собственными ограничениями сфера снова может свободно перемещаться за края плоскости, после чего она падает из-за силы тяжести. Это происходит потому, что мы никогда не переопределяем положение Y сферы.
Нам больше не нужны параметры конфигурации для разрешенной области. В нашем обычном бодрости больше нет необходимости.
// [SerializeField, Range (0f, 1f)]
// float bounciness = 0.5f;
// [SerializeField]
// Rect allowArea = new Rect (-5f, -5f, 10f, 10f);
Если мы все еще хотим ограничить сферу, чтобы она оставалась на плоскости, мы можем сделать это, добавив другие объекты, чтобы заблокировать ее путь. Например, создайте четыре куба, масштабируя и располагая их так, чтобы они образовывали стену вокруг плоскости. Это предотвратит падение сферы, хотя при столкновении со стенами ведет себя странно. Поскольку на данный момент у нас есть трехмерная геометрия, также неплохо бы снова включить тени, чтобы лучше понять глубину.
При попытке переместиться в угол сфера становится нервной, поскольку физический движок и наш собственный код борются за позиционирование сферы. Мы перемещаем его в стену, затем PhysX разрешает столкновение, выталкивая его обратно. Если мы перестанем проталкивать его в стену, то PhysX будет поддерживать движение сферы за счет импульса.
Controlling Rigidbody Velocity, Управление скоростью твердого тела
Если мы хотим использовать физический движок, мы должны позволить ему контролировать положение нашей сферы. Корректировка положения напрямую будет эффективно телепортироваться, а это не то, что нам нужно. Вместо этого мы должны косвенно контролировать сферу, применяя силы к ней или регулируя ее скорость.
У нас уже есть этот косвенный контроль над положением, поскольку мы вместо этого влияем на скорость. Все, что нам нужно сделать, это изменить наш код, чтобы он переопределял скорость
Rigidbody
компонента, а не настраивал положение самостоятельно. Для этого нам необходим доступ к компоненту, поэтому следите за ним через body
поле, которое инициализируется в Awake
методе. Твердое тело;
void Awake () {
body = GetComponent < Rigidbody > ();
}
Удалите код смещения
Update
и вместо этого назначьте нашу скорость телу. // Vector3 смещение = скорость * Time.deltaTime;
// Vector3 newPosition = transform.localPosition + displacement;
//transform.localPosition = newPosition;
body.velocity = скорость;
Но физические столкновения и тому подобное также влияют на скорость, поэтому извлеките ее из тела, прежде чем настраивать ее в соответствии с желаемой скоростью.
скорость = тело. скорость;
float maxSpeedChange = maxAcceleration * Time .deltaTime;
speed.x =
Mathf .MoveTowards (скорость.х, требуемая скорость.х, maxSpeedChange);
speed.z =
Mathf .MoveTowards (скорость.z, требуемая скорость.z, maxSpeedChange);
body.velocity = скорость;
Frictionless Movement, Движение без трения
Теперь мы отрегулируем скорость сферы, которую PhysX использует для ее перемещения. Затем разрешаются столкновения, которые могут регулировать скорость, которую мы затем настраиваем снова и так далее. Результирующее движение похоже на то, что было раньше, хотя сфера более медленная и не достигает максимальной скорости. Это потому, что PhysX применяет трение. Хотя это более реалистично, это усложняет настройку нашей сферы, поэтому давайте устраняем трения, а также упругость. Это делается путем создания нового физического материала с помощью Актив / Создание / Физический материал - да, в меню он записан как Физика - и установки всех значений на ноль и режимов объединения на Минимум .
Назначьте этот физический материал на коллайдер сферы.
Теперь он больше не подвержен никаким трениям и прыганиям.
Может показаться, что сфера все еще слегка подпрыгивает при столкновении со стеной. Это происходит потому, что PhysX не предотвращает столкновения, вместо этого он обнаруживает их после того, как они произошли, а затем перемещает твердые тела, чтобы они больше не пересекались. В случае быстрого движения это может занять более одного шага физического моделирования, поэтому мы можем видеть, как эта зависимость происходит.
Если движение действительно быстрое, то сфера может в конечном итоге пройти сквозь стену или оказаться зависимой от другой стороны, что более вероятно при наличии тонкой стены. Вы можете предотвратить это, изменив режим обнаружения столкновений
Rigidbody
, но это обычно требуется только при очень быстром движении.
Кроме того , сфера в настоящее время выдвигается вместо роликов, так что мы могли бы также заморозить его вращение во всех измерениях, которые мы можем сделать с помощью ограничений , флажков в
Rigidbody
компоненте.Fixed Update, Исправлено обновление
Физический движок использует фиксированный временной шаг, независимо от частоты кадров. Хотя мы уже дали контроль над сферой PhysX, мы все же влияем на ее скорость. Для достижения наилучших результатов мы должны отрегулировать скорость в шаге с фиксированным шагом по времени. Мы делаем это, разделяя наш
Update
метод на две части. Часть, в которой мы проверяем ввод и устанавливаем желаемую скорость, может остаться, в Update
то время как регулировка скорости должна перейти к новому FixedUpdate
методу. Чтобы сделать эту работу, мы должны хранить желаемую скорость в поле. Скорость Vector3, желаемая скорость;
void Update () {
Vector2 playerInput;
playerInput.x = Input .GetAxis ( "Horizontal" );
playerInput.y = Input .GetAxis ( "Vertical" );
playerInput = Vector2 .ClampMagnitude (playerInput, 1f );
// Vector3 requiredVelocity =
требуемая скорость =
новый Vector3 (playerInput.x, 0f , playerInput.y) * maxSpeed;
}
void FixedUpdate () {
скорость = тело. скорость;
float maxSpeedChange = maxAcceleration * Time .deltaTime;
speed.x =
Mathf .MoveTowards (скорость.х, требуемая скорость.х, maxSpeedChange);
speed.z =
Mathf .MoveTowards (скорость.z, требуемая скорость.z, maxSpeedChange);
body.velocity = скорость;
}
FixedUpdate
Метод вызывается , в начале каждого шага физики моделирования. Как часто это происходит, зависит от временного шага, который по умолчанию составляет 0,02 (пятьдесят раз в секунду), но вы можете изменить его через настройки проекта Time или через Time.fixedDeltaTime
.
В зависимости от вашей частоты кадров
FixedUpdate
может вызываться ноль, один или несколько раз за вызов Update
. Каждый кадр происходит последовательность FixedUpdate
вызовов, затем Update
вызывается, а затем кадр отображается. Это может сделать дискретную природу физического моделирования очевидной, когда временной шаг физики слишком велик по сравнению со временем кадра.
Вы можете решить это, уменьшив фиксированный временной шаг или включив режим интерполяции a
Rigidbody
. Установка значения Interpolate делает линейную интерполяцию между его последней и текущей позицией, поэтому он будет немного отставать от своего фактического положения в соответствии с PhysX. Другой вариант - Экстраполировать , который интерполирует свое предполагаемое положение в соответствии с его скоростью, что действительно приемлемо только для объектов, которые в основном имеют постоянную скорость.
Обратите внимание, что увеличение шага по времени означает, что сфера покрывает большее расстояние на обновление физики, что может привести к ее туннелированию через стены при использовании дискретного обнаружения столкновений.
Jumping, Прыжки
Поскольку наша сфера теперь может перемещаться по миру трехмерной физики, давайте дадим ей возможность прыгать.
Jumping on Command, Прыжки по команде
Мы можем использовать, чтобы определить, нажал ли игрок кнопку перехода для этого кадра, которая является клавишей пробела по умолчанию. Мы делаем это в , но, как и в случае с регулировкой скорости, мы откладываем фактический скачок до следующего вызова . Так что следите за тем, нужен ли переход через логическое поле.
Input.GetButtonDown("Jump")
Update
FixedUpdate
desiredJump
bool требуемый прыжок;
…
void Update () {
…
wantedJump = Input .GetButtonDown ( "Jump" );
}
Но мы могли бы в конечном итоге не вызывать
FixedUpdate
следующий кадр, в этом случае desiredJump
возвращаемое значение назад false
и желаемый прыжок будет забыт. Мы можем предотвратить это, комбинируя проверку с ее предыдущим значением с помощью логической операции ИЛИ или присвоения ИЛИ. Таким образом, он остается true
включенным до тех пор, пока мы явно не установим его обратно false
. desiredJump |= Input .GetButtonDown ( "Jump" );
Проверьте, нужен ли прыжок после регулировки скорости и перед ее применением
FixedUpdate
. Если это так, сбросьте desiredJump
и вызовите новый Jump
метод, который первоначально просто добавляет 5 к компоненту скорости Y, имитируя внезапное ускорение вверх. void FixedUpdate () {
…
if (requiredJump) {
wantedJump = false ;
Прыгать();
}
body.velocity = скорость;
}
void Jump () {
speed.y + = 5f ;
}
Это заставит сферу двигаться вверх, пока она неизбежно не упадет из-за силы тяжести.
Jump Height, Высота прыжка
Давайте настроим, насколько высоко наша сфера может прыгать. Мы могли бы сделать это, непосредственно управляя скоростью прыжка, но это не интуитивно понятно, поскольку связь между начальной скоростью прыжка и высотой прыжка не тривиальна. Удобнее напрямую контролировать высоту прыжка, так что давайте сделаем это.
[ SerializeField , Range ( 0f , 10f )]
float jumpHeight = 2f ;
Прыжки требуют преодоления силы тяжести, поэтому от этого зависит вертикальная скорость. В частности, где это сила тяжести и желаемая высота Знак минус там, потому что предполагается отрицательным. Мы можем получить его через
Physics.gravity.y
, который также может быть настроен через настройки проекта Physics . Мы используем вектор гравитации по умолчанию, который равен 9,81, что соответствует средней гравитации Земли. void Jump () {
speed.y + = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
}
Обратите внимание, что мы, скорее всего, немного отходим от желаемой высоты из-за дискретного характера физического моделирования. Максимум будет достигнут где-то между временными шагами.
Jumping While on the Ground, Прыжки на земле
В настоящее время мы можем прыгнуть в любой момент, даже находясь в воздухе, что позволяет оставаться в воздухе навсегда. Правильный прыжок может быть начат только тогда, когда сфера находится на земле. Мы не можем напрямую спросить
Rigidbody
, касается ли оно в настоящее время земли, но мы можем получить уведомление, когда оно столкнулось с чем-то, поэтому мы будем использовать это.
Если
MovingSphere
есть OnCollisionEnter
метод, то он будет вызван после того, как PhysX обнаружит новое столкновение. Столкновение остается живым, пока объекты находятся в контакте с другим. После этого OnCollisionExit
метод вызывается, если он существует. Добавьте оба метода в MovingSphere
, установив новое onGround
логическое поле true
в первом и false
в последнем. bool onGround;
…
void OnCollisionEnter () {
onGround = true ;
}
void OnCollisionExit () {
onGround = false ;
}
Теперь мы можем прыгать только тогда, когда мы на земле, что на данный момент мы предполагаем, что это тот случай, когда мы что-то трогаем. Если мы ни с чем не контактируем, то желаемый прыжок следует игнорировать.
void Jump () {
if (onGround) {speed.y
+ = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
}
}
Это работает, когда сфера касается только плоскости земли, но если она также ненадолго касается стены, то прыжки станут невозможными. Это происходит потому,
OnCollisionExit
что нас вызвали за стену, пока мы все еще контактируем с землей. Решение состоит в том, чтобы не полагаться на него, OnCollisionExit
а вместо этого добавить OnCollisionStay
метод, который вызывается на каждом физическом этапе, пока столкновение остается живым. Набор onGround
для true
в этом методе , а также. // void OnCollisionExit () {
// onGround = false;
//}
void OnCollisionStay () {
onGround = true ;
}
Каждый физический шаг начинается с вызова всех
FixedUpdate
методов, после чего PhysX делает свое дело, и в конце вызываются методы столкновения. Поэтому, когда FixedUpdate
вызывается, onGround
будет установлено значение true
на последнем шаге, если были какие-либо активные столкновения. Все, что нам нужно сделать, чтобы сохранить onGround
действительность, это установить его обратно false
в конце FixedUpdate
. void FixedUpdate () {
…
onGround = false ;
}
Теперь мы можем прыгать, пока мы находимся в контакте с чем-то.
No Wall Jumping, Не стена прыгает
Допуск прыжка при прикосновении к чему-либо означает, что мы могли бы прыгать и в воздухе, но прикасаясь к стене, а не к земле. Если мы хотим предотвратить это, мы должны уметь различать землю и что-то еще.
Имеет смысл определять землю как в основном горизонтальную плоскость. Мы могли бы проверить, удовлетворяет ли то, с чем мы сталкиваемся, этому критерию, проверив вектор нормалей контактных точек столкновения.
Простое столкновение имеет одну точку, где обе формы касаются, например, когда наша сфера касается плоскости земли. Обычно сфера немного проникала в плоскость, которую PhysX разрешил, отталкивая сферу прямо от плоскости. Направление толчка является нормальным вектором точки контакта. Поскольку мы используем сферу, этот вектор всегда указывает от точки контакта на поверхности сферы к ее центру.
В действительности это может стать более грязным, чем это, потому что может быть несколько столкновений, и проникновение может сохраняться в течение более чем одного шага моделирования, но нам не нужно беспокоиться об этом на данном этапе. Нам нужно понять, что одно столкновение может состоять из нескольких контактов. Это невозможно для столкновения плоскость и сфера, но возможно, когда задействован вогнутый коллайдер.
Мы можем получить информацию о столкновении, добавив
Collision
параметр к обоим OnCollisionEnter
и OnCollisionStay
. Вместо непосредственной установки onGround
на true
мы будем направлять эту ответственность на новый EvaluateCollision
метод, передавая ему данные. void OnCollisionEnter (Столкновение Столкновение) {
// onGround = true;
EvaluateCollision (столкновение);
}
void OnCollisionStay (Столкновение Столкновение) {
// onGround = true;
EvaluateCollision (столкновение);
}
void EvaluateCollision ( Collision collision) {}
Количество точек контакта можно узнать через
contactCount
свойство Collision
. Мы можем использовать это для GetContact
прохождения всех точек через метод, передавая ему индекс. Тогда мы можем получить доступ к normal
свойству точки . void EvaluateCollision ( Collision collision) {
for ( int i = 0 ; i <collision.contactCount; i ++) {
Vector3 normal = collision.GetContact (i) .normal;
}
}
Нормаль - это направление, в которое нужно толкать сферу, которое находится прямо от поверхности столкновения. Предполагая, что это плоскость, вектор соответствует нормальному вектору плоскости. Если бы плоскость была горизонтальной, то ее нормаль указывала бы прямо вверх, поэтому ее компонента Y должна быть ровно 1. Если это так, то мы касаемся земли. Но давайте будем снисходительными и примем Y-компоненты, которые равны 0,9 или больше.
Vector3 normal = collision.GetContact (i) .normal;
onGround | = normal.y> = 0.9f ;
Air Jumps, Воздушные прыжки
В этот момент мы можем прыгать только на земле, но игры часто допускают двойные или даже тройные прыжки в воздухе. Давайте поддержим это и сделаем настраиваемым, сколько воздушных прыжков разрешено.
[ SerializeField , Range ( 0 , 5 )]
int maxAirJumps = 0 ;
Теперь нам нужно отслеживать фазу прыжка, чтобы мы знали, разрешен ли другой прыжок. Мы можем сделать это через целочисленное поле, которое мы устанавливаем в ноль в начале,
FixedUpdate
если мы на земле. Но давайте перенесем этот код вместе с поиском скорости в отдельный UpdateState
метод, чтобы сократить FixedUpdate
. int jumpPhase;
…
void FixedUpdate () {
// скорость = тело. скорость;
UpdateState ();
…
}
void UpdateState () {
скорость = тело. скорость;
if (onGround) {
jumpPhase = 0 ;
}
}
С этого момента мы увеличиваем фазу прыжка при каждом прыжке. И нам разрешают прыгать, даже если мы не достигли максимально допустимых воздушных прыжков.
void Jump () {
if (onGround|| jumpPhase <maxAirJumps) {
jumpPhase + = 1 ;speed.y
+ = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
}
}
Limiting Upward Velocity, Ограничение скорости вверх
Быстрые воздушные прыжки позволяют достичь скорости, намного превышающей скорость одиночного прыжка. Мы собираемся изменить это, чтобы мы не могли превысить скорость прыжка, необходимую для достижения желаемой высоты одним прыжком. Первым шагом является выделение рассчитанной скорости прыжка в
Jump
. jumpPhase + = 1 ;
float jumpSpeed = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
speed.y + = jumpSpeed;
Если у нас уже есть скорость повышения, вычтите ее из скорости прыжка, прежде чем добавить ее к Y-компоненту скорости. Таким образом, мы никогда не превысим скорость прыжка.
float jumpSpeed = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
if ( speed.y > 0f ) {
jumpSpeed = jumpSpeed - speed.y;
}
speed.y + = jumpSpeed;
Но если мы уже идем быстрее, чем скорость прыжка, мы не хотим, чтобы прыжок замедлял нас. Мы можем предотвратить это, гарантируя, что измененная скорость прыжка никогда не станет отрицательной. Это сделано, беря максимум измененной скорости прыжка и ноль.
if ( speed.y > 0f ) {
jumpSpeed = Mathf .Max (jumpSpeed - speed.y, 0f );
}
Air Movement, Воздушное движение
В настоящее время нас не волнует, находится ли сфера на земле или в воздухе при управлении ею, но может иметь смысл, что воздушную сферу труднее контролировать. Уровень контроля может варьироваться от одного до полного. Это зависит от игры. Итак, давайте сделаем его настраиваемым, добавив отдельное максимальное воздушное ускорение, по умолчанию равное 1. Это резко снижает контроль в воздухе, но не снимает его полностью.
[ SerializeField , Range ( 0f , 100f )]
float maxAcceleration = 10f, maxAirAcceleration = 1f;
Какое ускорение мы используем при расчете изменения максимальной скорости,
FixedUpdate
зависит от того, находимся мы на земле или нет. Поплавок ускорение = onGround; maxAcceleration: maxAirAcceleration;
float maxSpeedChange =ускорение* Время .deltaTime;
Slopes, Склоны
Мы используем физику, чтобы переместить нашу сферу на маленькую плоскую плоскость, столкнуться со стенами и прыгать вокруг. Все это прекрасно работает, поэтому пришло время рассмотреть более сложные среды. В оставшейся части этого урока мы рассмотрим основные движения, когда задействованы склоны.
ProBuilder Test Scene, Тестовая сцена ProBuilder
Вы можете создать уклон, вращая плоскость или куб, но это неудобный способ создания уровня. Поэтому мы импортируем пакет ProBuilder и используем его для создания некоторых уклонов. Пакет ProGrids также удобен для привязки к сетке, хотя в Unity 2019.3 он не нужен, если вам случится это использовать. ProBuilder довольно прост в использовании, но может занять некоторое время, чтобы освоиться. Я не буду объяснять, как его использовать, просто имейте в виду, что речь идет в основном о гранях, причем ребра и вершины являются вторичными.
Я создал уклон, начав с куба ProBuilder , растянув его до 10 × 5 × 3, вытянув его еще на 10 единиц в измерении X, и свернув грани X до их нижних краев. Это создает треугольную двойную рампу с уклонами с обеих сторон длиной десять единиц и высотой 5 единиц.
Я поместил десять из них рядом друг с другом на плоскости, варьируя их высоту от одного до десяти единиц. Включая плоскую поверхность, это дает нам склоны с углами примерно 0,0 °, 5,7 °, 11,3 °, 16,7 °, 21,8 °, 26,6 °, 31,0 °, 35,0 °, 38,7 °, 42,0 ° и 45,0 °.
После этого я разместил еще десять склонов, на этот раз начиная с версии 45 ° и вытягивая наконечник влево на одну единицу на склон, пока я не получил вертикальную стену. Это дает нам углы, которые примерно равны 48,0 °, 51,3 °, 55,0 °, 59,0 °, 63,4 °, 68,2 °, 73,3 °, 78,7 °, 84,3 ° и 90,0 °.
Я закончил тестовый уровень, превратив нашу сферу в префаб и добавив 21 экземпляр, по одному на наклон, от полностью горизонтального до полностью вертикального.
Если вы не хотите создавать уровень самостоятельно, вы можете взять его из репозитория этого руководства .
Тест на уклон
Поскольку все сферные экземпляры реагируют на ввод пользователя, мы можем контролировать их все одновременно. Это позволяет проверить поведение сферы при одновременном взаимодействии со многими углами наклона. Для большинства этих тестов я войду в режим воспроизведения, а затем постоянно нажимаю вправо.
При стандартной конфигурации сфер мы видим, что первые пять сфер движутся с почти одинаковой горизонтальной скоростью независимо от угла наклона. Шестое едва проходит через немного позже, в то время как остальные отступают или полностью блокируются крутыми склонами.
Поскольку большинство сфер эффективно оказываются в воздухе, давайте установим максимальное воздушное ускорение на ноль. Таким образом, мы учитываем ускорение только на основании.
Разница между одним и нулевым ускорением воздуха не имеет большого значения для сфер, которые летят, потому что они запускаются с рампы. Но шестая сфера теперь больше не переходит на другую сторону, и другие сферы также раньше были остановлены гравитацией. Это произошло потому, что их склоны слишком крутые, чтобы сохранять достаточный импульс. В случае с шестой сферой его воздушное ускорение было достаточным, чтобы толкнуть его вверх.
Земной угол
В настоящее время мы используем 0,9 в качестве порога для классификации чего-либо как наземного или нет, но это произвольно. Мы могли бы использовать любой порог в диапазоне 0–1. Попытка обеих крайностей дает очень разные результаты.
Давайте сделаем порог настраиваемым, контролируя максимальный угол наклона, поскольку он более интуитивно понятен, чем Y-компонент вектора нормали склона. Давайте использовать 25 ° по умолчанию.
[ SerializeField , Range ( 0 , 90 )]
float maxGroundAngle = 25f ;
Когда поверхность горизонтальна, компонент Y ее вектора нормали равен 1. Для идеально вертикальной стенки компонент Y равен нулю. Компонент Y изменяется между этими крайностями в зависимости от угла наклона: это косинус угла. Здесь мы имеем дело с единичной окружностью, где Y - вертикальная ось, а горизонтальная ось лежит где-то в плоскости XZ. Другой способ сказать, что мы смотрим на скалярное произведение вектора вверх и нормали поверхности.
Настроенный угол определяет минимальный результат, который все еще считается как земля. Давайте хранить порог в поле и вычислить его с помощью
Mathf.Cos
в OnValidate
методе. Таким образом, он остается синхронизированным с углом, когда мы меняем его через инспектора в режиме воспроизведения. Также вызовите его, Awake
чтобы он вычислялся в сборках. float minGroundDotProduct;
void OnValidate () {
minGroundDotProduct = Mathf .Cos (maxGroundAngle);
}
void Awake () {
body = GetComponent < Rigidbody > ();
OnValidate ();
}
Мы указываем угол в градусах, но ожидаем,
Mathf.Cos
что он будет выражен в радианах. Мы можем преобразовать его, умножив на Mathf.Deg2Rad
. minGroundDotProduct = Mathf .Cos (maxGroundAngle* Mathf .Deg2Rad);
Теперь мы можем отрегулировать максимальный угол земли и посмотреть, как это влияет на движение сфер. Теперь я установлю угол на 40 °.
void EvaluateCollision ( Collision collision) {
for ( int i = 0 ; i <collision.contactCount; i ++) {
Vector3 normal = collision.GetContact (i) .normal;
onGround | = normal.y> = minGroundDotProduct;
}
}
Прыжки на склоне
Наши сферы всегда прыгают прямо вверх, независимо от угла наклона земли, на котором они сейчас находятся.
Другой подход заключается в том, чтобы спрыгнуть с поверхности земли в направлении ее нормального вектора. Это приведет к различным прыжкам на полосе тестирования на склоне, так что давайте сделаем это.
Нам нужно отслеживать текущий контакт в нормальном состоянии в поле и сохранять его всякий раз, когда мы сталкиваемся с контактом заземления
EvaluateCollision
. Vector3 contactNormal;
…
void EvaluateCollision ( Collision collision) {
for ( int i = 0 ; i <collision.contactCount; i ++) {
Vector3 normal = collision.GetContact (i) .normal;
// onGround | = normal.y> = minGroundDotProduct;
if (normal.y> = minGroundDotProduct) {
onGround = true ;
contactNormal = нормальный;
}
}
}
Но мы можем не коснуться земли. В этом случае мы будем использовать вектор вверх для нормального контакта, поэтому воздушные скачки все еще идут прямо вверх. Установите это в
UpdateState
случае необходимости. void UpdateState () {
скорость = тело. скорость;
if (onGround) {
jumpPhase = 0 ;
}
еще {
contactNormal = Vector3 .up;
}
}
Теперь мы должны добавить нормальный контакт прыжка, масштабированный по скорости прыжка, к скорости при прыжке, вместо того, чтобы всегда только увеличивать компонент Y. Это означает, что высота прыжка является показателем того, насколько высоко мы прыгаем, когда на ровной поверхности или только в воздухе. Прыжки на склоне не будут такими высокими, но будут влиять на горизонтальную скорость.
void Jump () {
if (onGround || jumpPhase <maxAirJumps) {
…
//velocity.y + = jumpSpeed;
скорость + = contactNormal * jumpSpeed;
}
}
Но это означает, что проверка положительной вертикальной скорости также больше не верна. Это должно стать проверкой скорости, выровненной с нормальным контактом. Мы можем найти эту скорость, проецируя скорость на нормаль контакта, вычисляя их точечное произведение через
Vector3.Dot
. float jumpSpeed = Mathf .Sqrt (- 2f * Physics .gravity.y * jumpHeight);
float alignSpeed = Vector3 .Dot (скорость, contactNormal);
если (alignedSpeed> 0f ) {
jumpSpeed = Mathf .Max (jumpSpeed -alignedSpeed, 0f );
}
скорость + = contactNormal * jumpSpeed;
Теперь, когда прыжки выровнены по наклонам, каждая сфера на нашем тестовом уровне получает уникальную траекторию прыжка. Сферы на более крутых склонах больше не прыгают прямо на свои склоны, а замедляются, поскольку прыжок толкает их в направлении, противоположном движению. Вы можете увидеть это более четко на всех склонах, испытав это с резко уменьшенной максимальной скоростью.
Перемещение по склонам
До сих пор мы всегда определяли желаемую скорость в горизонтальной плоскости XZ, независимо от угла наклона. Если сфера идет вверх по склону, это потому, что PhysX толкает ее вверх, чтобы разрешить столкновение, которое произошло, потому что мы дали ей горизонтальную скорость, которая указывает на наклон. Это может хорошо работать при подъеме по склонам, но при спуске по склонам сферы отодвигаются от земли и могут в конечном итоге упасть, когда их ускорение достаточно велико. Результатом является бодрое движение, которое трудно контролировать. Это хорошо видно при изменении направления движения при движении вверх по склонам, особенно при установке максимального ускорения на высокое значение.
Мы можем избежать этого, совместив желаемую скорость с землей. Это работает аналогично тому, как мы спроецировали скорость на нормаль, чтобы получить скорость прыжка, только теперь нам нужно спроецировать скорость на плоскость, чтобы получить новую скорость. Мы делаем это, беря скалярное произведение вектора и нормали, как и прежде, а затем вычитая нормаль, масштабированную по этому, из исходного вектора скорости. Давайте создадим
ProjectOnContactPlane
метод для того, что делает это с произвольным параметром вектора. Vector3 ProjectOnContactPlane ( вектор Vector3 ) {
обратный вектор - contactNormal * Vector3 .Dot (vector, contactNormal);
}
Давайте создадим новый
AdjustVelocity
метод для настройки скорости. Начните с определения проецируемых осей X и Z, проецируя правый и прямой векторы на контактную плоскость. void AdjustVelocity () {
Vector3 xAxis = ProjectOnContactPlane ( Vector3 .right);
Vector3 zAxis = ProjectOnContactPlane ( Vector3 .forward);
}
Это дает нам векторы, выровненные относительно земли, но они имеют длину только на единицу, когда земля идеально ровная. В общем, мы должны нормализовать векторы, чтобы получить правильные направления.
Vector3 xAxis = ProjectOnContactPlane ( Vector3 .right).normalized;
Vector3 zAxis = ProjectOnContactPlane ( Vector3 .forward).normalized;
Теперь мы можем спроецировать текущую скорость на оба вектора, чтобы получить относительные скорости X и Z.
Vector3 xAxis = ProjectOnContactPlane ( Vector3 .right) .normalized;
Vector3 zAxis = ProjectOnContactPlane ( Vector3 .forward) .normalized;
плавающий токX = Vector3 .Dot (скорость, xAxis);
плавающий ток Z = Vector3. Dot (скорость, ось Z);
Мы можем использовать их для расчета новых скоростей X и Z, как и раньше, но теперь относительно земли.
плавающий токX = Vector3 .Dot (скорость, xAxis);
плавающий ток Z = Vector3. Dot (скорость, ось Z);
Поплавок ускорение = onGround; maxAcceleration: maxAirAcceleration;
float maxSpeedChange = ускорение * время. deltaTime;
float newX =
Mathf .MoveTowards (currentX, wantedVelocity.x, maxSpeedChange);
float newZ =
Mathf .MoveTowards (currentZ, requiredVelocity.z, maxSpeedChange);
Наконец, скорректируйте скорость, добавив разницу между новой и старой скоростями по относительным осям.
float newX =
Mathf .MoveTowards (currentX, wantedVelocity.x, maxSpeedChange);
float newZ =
Mathf .MoveTowards (currentZ, requiredVelocity.z, maxSpeedChange);
скорость + = xAxis * (newX - currentX) + zAxis * (newZ - currentZ);
Запустите этот новый метод
FixedUpdate
вместо старого кода регулировки скорости. void FixedUpdate () {
UpdateState ();
AdjustVelocity ();
// float ускорение = onGround? maxAcceleration: maxAirAcceleration;
// float maxSpeedChange = ускорение * Time.deltaTime;
//velocity.x =
// Mathf.MoveTowards (speed.x, wantedVelocity.x, maxSpeedChange);
//velocity.z =
// Mathf.MoveTowards (speed.z, wantedVelocity.z, maxSpeedChange);
if (requiredJump) {
wantedJump = false ;
Прыгать();
}
body.velocity = скорость;
onGround = false ;
}
С нашим новым подходом к регулировке скорости сферы больше не теряют контакта с землей, когда внезапно меняют направление при движении вверх по склону. Кроме того, поскольку желаемая скорость регулирует свое направление в соответствии с наклоном, абсолютная желаемая горизонтальная скорость теперь меняется для каждой полосы движения.
Обратите внимание, что если наклон не совпадает с осью X или Z, угол между относительными проецируемыми осями не будет 90 °. Это не очень заметно, если склоны довольно крутые. Вы все еще можете двигаться во всех направлениях, но в некоторых направлениях будет сложнее точно управлять, чем в других. Это в некоторой степени имитирует неловкость попыток пройти по ней, но не выровнена по крутому склону.
Несколько наземных нормалей
Использование нормального контакта для регулировки желаемой скорости и направления прыжка прекрасно работает, когда существует только одна точка контакта с землей, но поведение может стать странным и непредсказуемым, если одновременно существует несколько контактов заземления. Чтобы проиллюстрировать это, я создал еще одну тестовую сцену с несколькими углублениями в земле, позволяющую использовать до четырех точек контакта одновременно.
Во время прыжков в каком направлении будут двигаться сферы? В моем случае те, у кого четыре контакта, предпочитают одно направление, но могут в конечном итоге идти в четырех разных направлениях. Аналогично, сфера с двумя контактами произвольно выбирается между двумя направлениями. И сферы с тремя контактами последовательно перепрыгивают одинаково, чтобы соответствовать соседним сферам, которые касаются только одного склона.
Такое поведение проявляется потому, что мы устанавливаем контакт
EvaluateCollision
всякий раз, когда находим контакт с землей. Так что, если мы найдем несколько, победит последний. Порядок либо произвольный из-за движения, либо всегда один и тот же из-за порядка, в котором PhysX оценивает столкновения.
Какое направление лучше? Там нет ни одного. Наиболее разумно объединить их все в одну нормаль, которая представляет среднюю плоскость земли. Для этого нам нужно накапливать нормальные векторы. Это требует, чтобы мы установили нормальный контакт в ноль в конце
FixedUpdate
. Давайте поместим код для этого вместе со сбросом onGround
в новый ClearState
метод. void FixedUpdate () {
…
body.velocity = скорость;
// onGround = false;
ClearState ();
}
void ClearState () {
onGround = false ;
contactNormal = Vector3 .zero;
}
Теперь накапливайте нормали
EvaluateCollision
вместо того, чтобы переопределять предыдущий. void EvaluateCollision ( Collision collision) {
for ( int i = 0 ; i <collision.contactCount; i ++) {
Vector3 normal = collision.GetContact (i) .normal;
if (normal.y> = minGroundDotProduct) {
onGround = true ;
contactNormal += обычный;
}
}
}
Наконец, нормализуйте нормальный контакт,
UpdateState
когда он на земле, чтобы сделать его нормальным вектором нормалей. void UpdateState () {
скорость = тело. скорость;
если (onGround) {
jumpPhase = 0 ;
contactNormal.Normalize ();
}
еще {
contactNormal = Vector3 .up;
}
}
Подсчет наземных контактов
Хотя это и необязательно, мы могли бы подсчитать, сколько у нас точек контакта с землей, вместо того, чтобы просто отслеживать, есть ли хотя бы одна. Мы делаем это, заменяя логическое поле целым числом. Затем мы вводим логическое
OnGround
свойство только для чтения (обратите внимание на заглавные буквы), которое проверяет, является ли счет больше нуля, заменяя onGround
поле. // bool onGround;
int groundContactCount;
bool OnGround => groundContactCount> 0 ;
ClearState
Теперь нужно установить счетчик на ноль. void ClearState () {
// onGround = false;
groundContactCount = 0 ;
contactNormal = Vector3 .zero;
}
И
UpdateState
приходится полагаться на собственность, а не на поле. Кроме того, мы также можем немного оптимизировать его, потрудившись лишь нормализовать нормальный контакт, если он агрегатный, так как в противном случае он уже имеет единичную длину. void UpdateState () {
скорость = тело. скорость;
если (На земле) {
jumpPhase = 0 ;
if (groundContactCount> 1 ) {
contactNormal.Normalize ();
}
}
…
}
Также
Evaluate
при необходимости увеличьте количество. void EvaluateCollision ( Collision collision) {
for ( int i = 0 ; i <collision.contactCount; i ++) {
Vector3 normal = collision.GetContact (i) .normal;
if (normal.y> = minGroundDotProduct) {
// onGround = true;
groundContactCount + = 1 ;
contactNormal + = нормальный;
}
}
}
Наконец, заменить
onGround
на OnGround
в AdjustVelocity
и Jump
.
Кроме того, оптимизация
UpdateState
числа контактов с землей также может быть полезна для отладки. Например, вы можете либо зарегистрировать счетчик, либо настроить цвет сферы на основе счетчика, чтобы лучше понять его состояние.
В следующем уроке мы еще больше расширим возможности движения в нашей сфере. Хотите знать, когда он выйдет? Следите за обновлениями на моей странице Patreon !
Комментариев нет:
Отправить комментарий