11/08/2019

Лучшие уроки Unity от Джаспера Флика

Написание лучших уроков Unity
Здравствуйте! Я Джаспер Флик из Catlike Coding .
Я делаю высококачественные учебные пособия, которые научат вас всему о C # и программировании шейдеров с помощью Unity. Мои учебники написаны, потому что я считаю, что текст является лучшим носителем для этой темы. Они также содержат короткие анимации, когда я думаю, что они добавляют ценность.

                             



Физика толкает сферу

Управляйте скоростью сферы твердого тела.
Поддержка вертикального движения с помощью прыжков.
Определить землю и ее угол.
Используйте ProBuilder для создания тестовых уровней.
Двигаться по склонам.
Это вторая часть серии руководств по управлению движением персонажа. На этот раз мы будем использовать физический движок для создания более реалистичного движения и поддержки более сложных сред.
Этот учебник сделан с 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вызывается, а затем кадр отображается. Это может сделать дискретную природу физического моделирования очевидной, когда временной шаг физики слишком велик по сравнению со временем кадра.
0.2 физический шаг по времени.
Вы можете решить это, уменьшив фиксированный временной шаг или включив режим интерполяции a RigidbodyУстановка значения Interpolate делает линейную интерполяцию между его последней и текущей позицией, поэтому он будет немного отставать от своего фактического положения в соответствии с PhysX. Другой вариант - Экстраполировать , который интерполирует свое предполагаемое положение в соответствии с его скоростью, что действительно приемлемо только для объектов, которые в основном имеют постоянную скорость.
0.2 физический шаг по времени с интерполяцией.
Обратите внимание, что увеличение шага по времени означает, что сфера покрывает большее расстояние на обновление физики, что может привести к ее туннелированию через стены при использовании дискретного обнаружения столкновений.

Jumping, Прыжки

Поскольку наша сфера теперь может перемещаться по миру трехмерной физики, давайте дадим ей возможность прыгать.

Jumping on Command, Прыжки по команде

Мы можем использовать, чтобы определить, нажал ли игрок кнопку перехода для этого кадра, которая является клавишей пробела по умолчанию. Мы делаем это в , но, как и в случае с регулировкой скорости, мы откладываем фактический скачок до следующего вызова Так что следите за тем, нужен ли переход через логическое поле.Input.GetButtonDown("Jump")UpdateFixedUpdatedesiredJump
 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 ;
Высота прыжка.
Прыжки требуют преодоления силы тяжести, поэтому от этого зависит вертикальная скорость. В частности, vи=-2gчасгде gэто сила тяжести и часжелаемая высота Знак минус там, потому что gпредполагается отрицательным. Мы можем получить его через 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 единиц.
Пандус из двух склонов 10 × 5 × 3.
Я поместил десять из них рядом друг с другом на плоскости, варьируя их высоту от одного до десяти единиц. Включая плоскую поверхность, это дает нам склоны с углами примерно 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 экземпляр, по одному на наклон, от полностью горизонтального до полностью вертикального.
Уровень теста на склоне.
Если вы не хотите создавать уровень самостоятельно, вы можете взять его из репозитория этого руководства .

Тест на уклон

Поскольку все сферные экземпляры реагируют на ввод пользователя, мы можем контролировать их все одновременно. Это позволяет проверить поведение сферы при одновременном взаимодействии со многими углами наклона. Для большинства этих тестов я войду в режим воспроизведения, а затем постоянно нажимаю вправо.
Тест на уклон.
При стандартной конфигурации сфер мы видим, что первые пять сфер движутся с почти одинаковой горизонтальной скоростью независимо от угла наклона. Шестое едва проходит через немного позже, в то время как остальные отступают или полностью блокируются крутыми склонами.
Поскольку большинство сфер эффективно оказываются в воздухе, давайте установим максимальное воздушное ускорение на ноль. Таким образом, мы учитываем ускорение только на основании.
1 0
Воздушное ускорение один и ноль.
Разница между одним и нулевым ускорением воздуха не имеет большого значения для сфер, которые летят, потому что они запускаются с рампы. Но шестая сфера теперь больше не переходит на другую сторону, и другие сферы также раньше были остановлены гравитацией. Это произошло потому, что их склоны слишком крутые, чтобы сохранять достаточный импульс. В случае с шестой сферой его воздушное ускорение было достаточным, чтобы толкнуть его вверх.

Земной угол

В настоящее время мы используем 0,9 в качестве порога для классификации чего-либо как наземного или нет, но это произвольно. Мы могли бы использовать любой порог в диапазоне 0–1. Попытка обеих крайностей дает очень разные результаты.
1 0
Порог заземления один и ноль.
Давайте сделаем порог настраиваемым, контролируя максимальный угол наклона, поскольку он более интуитивно понятен, чем 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;
  }
 }
25 40
Максимальный угол наклона 25 и 40.

Прыжки на склоне

Наши сферы всегда прыгают прямо вверх, независимо от угла наклона земли, на котором они сейчас находятся.
Всегда прыгаю прямо.
Другой подход заключается в том, чтобы спрыгнуть с поверхности земли в направлении ее нормального вектора. Это приведет к различным прыжкам на полосе тестирования на склоне, так что давайте сделаем это.
Нам нужно отслеживать текущий контакт в нормальном состоянии в поле и сохранять его всякий раз, когда мы сталкиваемся с контактом заземления 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;
Спрыгивая с поверхности.
Теперь, когда прыжки выровнены по наклонам, каждая сфера на нашем тестовом уровне получает уникальную траекторию прыжка. Сферы на более крутых склонах больше не прыгают прямо на свои склоны, а замедляются, поскольку прыжок толкает их в направлении, противоположном движению. Вы можете увидеть это более четко на всех склонах, испытав это с резко уменьшенной максимальной скоростью.
Прыгать назад; максимальная скорость 1.

Перемещение по склонам

До сих пор мы всегда определяли желаемую скорость в горизонтальной плоскости XZ, независимо от угла наклона. Если сфера идет вверх по склону, это потому, что PhysX толкает ее вверх, чтобы разрешить столкновение, которое произошло, потому что мы дали ей горизонтальную скорость, которая указывает на наклон. Это может хорошо работать при подъеме по склонам, но при спуске по склонам сферы отодвигаются от земли и могут в конечном итоге упасть, когда их ускорение достаточно велико. Результатом является бодрое движение, которое трудно контролировать. Это хорошо видно при изменении направления движения при движении вверх по склонам, особенно при установке максимального ускорения на высокое значение.
Потеря контакта с землей; максимальное ускорение 100.
Мы можем избежать этого, совместив желаемую скорость с землей. Это работает аналогично тому, как мы спроецировали скорость на нормаль, чтобы получить скорость прыжка, только теперь нам нужно спроецировать скорость на плоскость, чтобы получить новую скорость. Мы делаем это, беря скалярное произведение вектора и нормали, как и прежде, а затем вычитая нормаль, масштабированную по этому, из исходного вектора скорости. Давайте создадим 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 ;
 }
Оставаться на одной линии с землей; максимальное ускорение 100.
С нашим новым подходом к регулировке скорости сферы больше не теряют контакта с землей, когда внезапно меняют направление при движении вверх по склону. Кроме того, поскольку желаемая скорость регулирует свое направление в соответствии с наклоном, абсолютная желаемая горизонтальная скорость теперь меняется для каждой полосы движения.
абсолютный относительный
Абсолютная и относительная желаемая скорость.
Обратите внимание, что если наклон не совпадает с осью 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 !
хранилищелицензийPDF

Комментариев нет:

Отправить комментарий