Ускорение в Единстве - PullRequest
       37

Ускорение в Единстве

0 голосов
/ 06 июня 2018

Я пытаюсь эмулировать ускорение и замедление в Unity.

Я написал код, чтобы сгенерировать трек в Unity и поместить объект в определенное место на треке на основе времени.Результат выглядит примерно так:

Cube mid way through Catmull-Rom Spline

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

Чтобы попытаться решить эту проблему, я попытался использовать Уравнения замедления Роберта Пеннера по методу GetTime(Vector3 p0, Vector3 p1, float alpha).Однако, хотя это помогло, этого было недостаточно.Между переходами все еще были скачки скорости.

Есть ли у кого-нибудь идеи о том, как я могу динамически ослабить положение куба, чтобы он выглядел как ускоряющийся и замедляющийся, без больших скачков скорости между сегментами?трека?


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

using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

public class InterpolationExample : MonoBehaviour {
    [Header("Time")]
    [SerializeField]
    private float currentTime;
    private float lastTime = 0;
    [SerializeField]
    private float timeModifier = 1;
    [SerializeField]
    private bool running = true;
    private bool runningBuffer = true;

    [Header("Track Settings")]
    [SerializeField]
    [Range(0, 1)]
    private float catmullRomAlpha = 0.5f;
    [SerializeField]
    private List<SimpleWayPoint> wayPoints = new List<SimpleWayPoint>
    {
        new SimpleWayPoint() {pos = new Vector3(-4.07f, 0, 6.5f), time = 0},
        new SimpleWayPoint() {pos = new Vector3(-2.13f, 3.18f, 6.39f), time = 1},
        new SimpleWayPoint() {pos = new Vector3(-1.14f, 0, 4.55f), time = 6},
        new SimpleWayPoint() {pos = new Vector3(0.07f, -1.45f, 6.5f), time = 7},
        new SimpleWayPoint() {pos = new Vector3(1.55f, 0, 3.86f), time = 7.2f},
        new SimpleWayPoint() {pos = new Vector3(4.94f, 2.03f, 6.5f), time = 10}
    };

    [Header("Debug")]
    [Header("WayPoints")]
    [SerializeField]
    private bool debugWayPoints = true;
    [SerializeField]
    private WayPointDebugType debugWayPointType = WayPointDebugType.SOLID;
    [SerializeField]
    private float debugWayPointSize = 0.2f;
    [SerializeField]
    private Color debugWayPointColour = Color.green;
    [Header("Track")]
    [SerializeField]
    private bool debugTrack = true;
    [SerializeField]
    [Range(0, 1)]
    private float debugTrackResolution = 0.04f;
    [SerializeField]
    private Color debugTrackColour = Color.red;

    [System.Serializable]
    private class SimpleWayPoint
    {
        public Vector3 pos;
        public float time;
    }

    [System.Serializable]
    private enum WayPointDebugType
    {
        SOLID,
        WIRE
    }

    private void Start()
    {
        wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
        wayPoints.Insert(0, wayPoints[0]);
        wayPoints.Add(wayPoints[wayPoints.Count - 1]);
    }

    private void LateUpdate()
    {
        //This means that if currentTime is paused, then resumed, there is not a big jump in time
        if(runningBuffer != running)
        {
            runningBuffer = running;
            lastTime = Time.time;
        }

        if(running)
        {
            currentTime += (Time.time - lastTime) * timeModifier;
            lastTime = Time.time;
            if(currentTime > wayPoints[wayPoints.Count - 1].time)
            {
                currentTime = 0;
            }
        }
        transform.position = GetPosition(currentTime);
    }

    #region Catmull-Rom Math
    public Vector3 GetPosition(float time)
    {
        //Check if before first waypoint
        if(time <= wayPoints[0].time)
        {
            return wayPoints[0].pos;
        }
        //Check if after last waypoint
        else if(time >= wayPoints[wayPoints.Count - 1].time)
        {
            return wayPoints[wayPoints.Count - 1].pos;
        }

        //Check time boundaries - Find the nearest WayPoint your object has passed
        float minTime = -1;
        float maxTime = -1;
        int minIndex = -1;
        for(int i = 1; i < wayPoints.Count; i++)
        {
            if(time > wayPoints[i - 1].time && time <= wayPoints[i].time)
            {
                maxTime = wayPoints[i].time;
                int index = i - 1;
                minTime = wayPoints[index].time;
                minIndex = index;
            }
        }

        float timeDiff = maxTime - minTime;
        float percentageThroughSegment = 1 - ((maxTime - time) / timeDiff);

        //Define the 4 points required to make a Catmull-Rom spline
        Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
        Vector3 p1 = wayPoints[minIndex].pos;
        Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
        Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;

        return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
    }

    //Prevent Index Out of Array Bounds
    private int ClampListPos(int pos)
    {
        if(pos < 0)
        {
            pos = wayPoints.Count - 1;
        }

        if(pos > wayPoints.Count)
        {
            pos = 1;
        }
        else if(pos > wayPoints.Count - 1)
        {
            pos = 0;
        }

        return pos;
    }

    //Math behind the Catmull-Rom curve. See here for a good explanation of how it works. https://stackoverflow.com/a/23980479/4601149
    private Vector3 GetCatmullRomPosition(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha)
    {
        float dt0 = GetTime(p0, p1, alpha);
        float dt1 = GetTime(p1, p2, alpha);
        float dt2 = GetTime(p2, p3, alpha);

        Vector3 t1 = ((p1 - p0) / dt0) - ((p2 - p0) / (dt0 + dt1)) + ((p2 - p1) / dt1);
        Vector3 t2 = ((p2 - p1) / dt1) - ((p3 - p1) / (dt1 + dt2)) + ((p3 - p2) / dt2);

        t1 *= dt1;
        t2 *= dt1;

        Vector3 c0 = p1;
        Vector3 c1 = t1;
        Vector3 c2 = (3 * p2) - (3 * p1) - (2 * t1) - t2;
        Vector3 c3 = (2 * p1) - (2 * p2) + t1 + t2;
        Vector3 pos = CalculatePosition(t, c0, c1, c2, c3);

        return pos;
    }

    private float GetTime(Vector3 p0, Vector3 p1, float alpha)
    {
        if(p0 == p1)
            return 1;
        return Mathf.Pow((p1 - p0).sqrMagnitude, 0.5f * alpha);
    }

    private Vector3 CalculatePosition(float t, Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3)
    {
        float t2 = t * t;
        float t3 = t2 * t;
        return c0 + c1 * t + c2 * t2 + c3 * t3;
    }

    //Utility method for drawing the track
    private void DisplayCatmullRomSpline(int pos, float resolution)
    {
        Vector3 p0 = wayPoints[ClampListPos(pos - 1)].pos;
        Vector3 p1 = wayPoints[pos].pos;
        Vector3 p2 = wayPoints[ClampListPos(pos + 1)].pos;
        Vector3 p3 = wayPoints[ClampListPos(pos + 2)].pos;

        Vector3 lastPos = p1;
        int maxLoopCount = Mathf.FloorToInt(1f / resolution);

        for(int i = 1; i <= maxLoopCount; i++)
        {
            float t = i * resolution;
            Vector3 newPos = GetCatmullRomPosition(t, p0, p1, p2, p3, catmullRomAlpha);
            Gizmos.DrawLine(lastPos, newPos);
            lastPos = newPos;
        }
    }
    #endregion

    private void OnDrawGizmos()
    {
        #if UNITY_EDITOR
        if(EditorApplication.isPlaying)
        {
            if(debugWayPoints)
            {
                Gizmos.color = debugWayPointColour;
                foreach(SimpleWayPoint s in wayPoints)
                {
                    if(debugWayPointType == WayPointDebugType.SOLID)
                    {
                        Gizmos.DrawSphere(s.pos, debugWayPointSize);
                    }
                    else if(debugWayPointType == WayPointDebugType.WIRE)
                    {
                        Gizmos.DrawWireSphere(s.pos, debugWayPointSize);
                    }
                }
            }

            if(debugTrack)
            {
                Gizmos.color = debugTrackColour;
                if(wayPoints.Count >= 2)
                {
                    for(int i = 0; i < wayPoints.Count; i++)
                    {
                        if(i == 0 || i == wayPoints.Count - 2 || i == wayPoints.Count - 1)
                        {
                            continue;
                        }

                        DisplayCatmullRomSpline(i, debugTrackResolution);
                    }
                }
            }
        }
        #endif
    }
}

Ответы [ 4 ]

0 голосов
/ 15 июня 2018

Хорошо, давайте добавим математику к этому.

Я всегда был сторонником важности и полезности математики в gamedev, и, возможно, я зайду слишком далеко в этомэто ответ, но я действительно думаю, что ваш вопрос вовсе не о кодировании, а о моделировании и решении проблемы алгебры.В любом случае, давайте пойдем.

Параметризация

Если у вас есть высшее образование, вы можете вспомнить кое-что о функциях - операциях, которые принимают параметр и дают результат -и graphs - графическое представление (или график) эволюции функции в зависимости от ее параметра.f(x) может напомнить вам кое-что: в нем говорится, что функция с именем f зависит от параметра x.Таким образом, «to parameterize » грубо означает выражение системы в виде одного или нескольких параметров.

Возможно, вы не ознакомлены с условиями, но вы делаете это постоянно.Например, Track - это система с 3 параметрами: f(x,y,z).

Одна интересная вещь в параметризации заключается в том, что вы можете захватить систему и описать ее в терминах других параметров.Опять же, вы уже делаете это.Когда вы описываете эволюцию вашего трека со временем, вы говорите, что каждая координата является функцией времени, f(x,y,z) = f(x(t),y(t),z(t)) = f(t).Другими словами, вы можете использовать время для вычисления каждой координаты и использовать координаты для позиционирования вашего объекта в пространстве в течение данного времени.

Моделирование системы треков

Наконец, я начнуотвечая на ваш вопрос.Чтобы полностью описать нужную вам систему треков, вам понадобятся две вещи:

  1. Путь;

Вы практически уже решили эту часть.Вы устанавливаете некоторые точки в пространстве сцены и используете сплайн Катмулла-Рома для интерполяции точек и создания пути.Это умно, и с этим ничего не поделаешь.

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

Движущийся объект.

Одна интересная вещь в вашем решении Path состоит в том, что вы параметризовали расчет пути с помощью параметра percentageThroughSegment - значение в диапазоне от 0 до 1, представляющее относительную позицию внутрисегмент.В вашем коде вы выполняете итерацию с фиксированными временными шагами, и ваше percentageThroughSegment будет пропорциональным соотношением между затраченным временем и общим временным интервалом сегмента.Поскольку каждый сегмент имеет определенный промежуток времени, вы эмулируете много постоянных скоростей.

Это довольно стандартно, но есть одна тонкость.Вы игнорируете чрезвычайно важную часть описания движения: пройденное расстояние .

Я предлагаю вам другой подход.Используйте пройденное расстояние для параметризации вашего пути.Тогда движение объекта будет параметризованным по времени пройденным расстоянием.Таким образом, у вас будет две независимые и согласованные системы.Руки на работу!

Пример:

С этого момента я сделаю все 2D для простоты, но позже его изменение на 3D будет тривиальным.

Рассмотрим следующий путь:

Example path

Где i - индекс отрезка, d - пройденное расстояние и x, y -координаты в плоскости.Это может быть путь, созданный таким сплайном, как у вас, или с кривыми Безье, или чем-то еще.

Движение, развиваемое объектом с вашим текущим решением, может быть описано как график distance traveled on the path против time какэто:

Movement 1 graph

Где t в таблице - это время, когда объект должен достичь чека, d - снова пройденное расстояниев этой позиции v - скорость, а a - ускорение.

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

Мы должны вспомнить некоторую физику в этой точке и отметить, что в каждом сегменте график расстояния представляет собой прямую линию, которая соответствует движению впостоянная скорость, без ускорения.Такая система описывается этим уравнением: d = do + v*t

Movement 1 animation

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

Хорошо, как мы можем сделать это лучше?Хм ... если бы график скорости был непрерывным, не было бы такого раздражающего скачка скорости.Самое простое описание такого движения может быть равномерно ускоренным.Такая система описывается этим уравнением: d = do + vo*t + a*t^2/2.Мы также должны будем принять начальную скорость, здесь я выберу ноль (отстранение от отдыха).

enter image description here

Как мы и ожидали, график скоростиявляется непрерывным, движение ускоряется через путь.Это может быть закодировано в Unity, изменяя метиды Start и GetPosition следующим образом:

private List<float> lengths = new List<float>();
private List<float> speeds = new List<float>();
private List<float> accels = new List<float>();
public float spdInit = 0;

private void Start()
{
  wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
  wayPoints.Insert(0, wayPoints[0]);
  wayPoints.Add(wayPoints[wayPoints.Count - 1]);
       for (int seg = 1; seg < wayPoints.Count - 2; seg++)
  {
    Vector3 p0 = wayPoints[seg - 1].pos;
    Vector3 p1 = wayPoints[seg].pos;
    Vector3 p2 = wayPoints[seg + 1].pos;
    Vector3 p3 = wayPoints[seg + 2].pos;
    float len = 0.0f;
    Vector3 prevPos = GetCatmullRomPosition(0.0f, p0, p1, p2, p3, catmullRomAlpha);
    for (int i = 1; i <= Mathf.FloorToInt(1f / debugTrackResolution); i++)
    {
      Vector3 pos = GetCatmullRomPosition(i * debugTrackResolution, p0, p1, p2, p3, catmullRomAlpha);
      len += Vector3.Distance(pos, prevPos);
      prevPos = pos;
    }
    float spd0 = seg == 1 ? spdInit : speeds[seg - 2];
    float lapse = wayPoints[seg + 1].time - wayPoints[seg].time;
    float acc = (len - spd0 * lapse) * 2 / lapse / lapse;
    float speed = spd0 + acc * lapse;
    lengths.Add(len);
    speeds.Add(speed);
    accels.Add(acc);
  }
}

public Vector3 GetPosition(float time)
{
  //Check if before first waypoint
  if (time <= wayPoints[0].time)
  {
    return wayPoints[0].pos;
  }
  //Check if after last waypoint
  else if (time >= wayPoints[wayPoints.Count - 1].time)
  {
    return wayPoints[wayPoints.Count - 1].pos;
  }

  //Check time boundaries - Find the nearest WayPoint your object has passed
  float minTime = -1;
  // float maxTime = -1;
  int minIndex = -1;
  for (int i = 1; i < wayPoints.Count; i++)
  {
    if (time > wayPoints[i - 1].time && time <= wayPoints[i].time)
    {
      // maxTime = wayPoints[i].time;
      int index = i - 1;
      minTime = wayPoints[index].time;
      minIndex = index;
    }
  }

  float spd0 = minIndex == 1 ? spdInit : speeds[minIndex - 2];
  float len = lengths[minIndex - 1];
  float acc = accels[minIndex - 1];
  float t = time - minTime;
  float posThroughSegment = spd0 * t + acc * t * t / 2;
  float percentageThroughSegment = posThroughSegment / len;

  //Define the 4 points required to make a Catmull-Rom spline
  Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
  Vector3 p1 = wayPoints[minIndex].pos;
  Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
  Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;

  return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}

Хорошо, давайте посмотрим, как это происходит ...

enter image description here

Э-э-э-э-э.Это выглядело почти хорошо, за исключением того, что в какой-то момент он движется назад, а затем снова продвигается.На самом деле, если мы проверим наши графики, это описано там.Между 12 и 16 секундами скорость равна нулю.Почему это происходит?Поскольку эта функция движения (постоянные ускорения), хотя и проста, имеет некоторые ограничения.При некоторых резких изменениях скорости может не существовать постоянного значения ускорения, которое может гарантировать нашу предпосылку (прохождение контрольных точек в правильное время) без побочных эффектов, подобных этим.

Что нам теперь делать?

У вас есть много вариантов:

  • Опишите систему с изменениями линейного ускорения и примените граничные условия (Предупреждение: много уравнений для решения);
  • Опишите систему с постоянным ускорением в течение некоторого периода времени, например, ускорение или замедление непосредственно перед / после кривой, затем сохраняйте постоянную скорость для остальной части сегмента (Предупреждение: еще больше уравнений для решения, трудногарантируйте посылку прохождения контрольных точек в правильное время);
  • Используйте метод интерполяции для создания графика положения во времени.Я попробовал сам Catmull-Rom, но мне не понравился результат, потому что скорость выглядела не очень плавно.Кривые Безье кажутся более предпочтительным подходом, потому что вы можете управлять склонами (иначе говоря, скоростями) на контрольных точках напрямую и избегать обратных движений;
  • И мой любимый: добавить общедоступное поле AnimationCurve в классе и настроитьваш график движения в редакторе с потрясающим встроенным ящиком!Вы можете легко добавить контрольные точки с помощью метода AddKey и выбрать позицию на некоторое время с помощью метода Evaluate.Вы даже можете использовать метод OnValidate в своем классе компонентов, чтобы автоматически обновлять точки в Сцене, когда вы редактируете ее на кривой и наоборот.

Не останавливайтесь на этом!Добавьте градиент на линию контура Gizmo, чтобы легко видеть, куда он идет быстрее или медленнее, добавьте маркеры для манипулирования траекторией в режиме редактора ... проявите творческий подход!

0 голосов
/ 12 июня 2018

Давайте сначала определим несколько терминов:

  1. t: переменная интерполяции для каждого сплайна, в диапазоне от 0 до 1.
  2. s: длинакаждого сплайна.В зависимости от того, какой тип сплайна вы используете (Catmull-rom, Bezier и т. Д.), Существуют формулы для расчета предполагаемой общей длины.
  3. dt: изменение t на кадр.В вашем случае, если она постоянна для всех различных сплайнов, вы увидите внезапное изменение скорости в конечных точках сплайна, поскольку каждый сплайн имеет различную длину s.

Самый простой способ облегчитьизменение скорости в каждом соединении:

void Update() {
    float dt = 0.05f; //this is currently your "global" interpolation speed, for all splines
    float v0 = s0/dt; //estimated linear speed in the first spline.
    float v1 = s1/dt; //estimated linear speed in the second spline.
    float dt0 = interpSpeed(t0, v0, v1) / s0; //t0 is the current interpolation variable where the object is at, in the first spline
    transform.position = GetCatmullRomPosition(t0 + dt0*Time.deltaTime, ...); //update your new position in first spline
}

, где:

float interpSpeed(float t, float v0, float v1, float tEaseStart=0.5f) {
    float u = (t - tEaseStart)/(1f - tEaseStart);
    return Mathf.Lerp(v0, v1, u);
}

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

Наконец, для того, чтобы облегчение выглядело еще лучше:

  • Рассмотрите возможность использования нелинейной интерполяционной функции в interpSpeed().
  • Подумайте о том, чтобы реализовать "облегчение" также в начале второго сплайна
0 голосов
/ 12 июня 2018

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

В нем есть некоторые переменные, которые вы можете отрегулировать вместе с переменными Rigidbody для достижения имитации вождения.

Когда они пишут

Вы можете иметь до 20 колес на одном экземпляре транспортного средства, каждое из которых применяет рулевое управление, двигатель или тормозной момент.

Отказ от ответственности: у меня только минимальный опытработа с WheelColliders.Но они кажутся мне тем, что вы ищете.

https://docs.unity3d.com/Manual/WheelColliderTutorial.html

enter image description here

0 голосов
/ 11 июня 2018

Насколько я могу судить, у вас уже есть большая часть решения, только неправильно инициализированная.

Локальная скорость зависит от длины сплайна, поэтому вы должны модулировать скорость на обратно пропорционально длине сегмента (которую вы можете легко аппроксимировать с помощью нескольких шагов),

Конечно, в вашем случае вы не можете контролировать скорость, только время ввода, так что вам нужно правильно распределить значения SimpleWayPoint.time в соответствии с порядком и длинойпредыдущие сплайновые сегменты вместо ручной инициализации в объявлении поля .Таким образом, percentageThroughSegment должен быть равномерно распределен.

Как уже упоминалось в комментариях, часть этой математики может выглядеть проще с Lerp():)

...