11

我试图在 Unity 中模拟加速和减速。

我编写了代码以在 Unity 中生成轨道,并根据时间将对象放置在轨道上的特定位置。结果看起来有点像这样。

立方通过 Catmull-Rom 样条中途

我目前遇到的问题是样条曲线的每个部分都有不同的长度,并且立方体以不同但均匀的速度穿过每个部分。这会导致在不同部分之间转换时立方体的速度变化会突然发生跳跃。

为了尝试解决这个问题,我尝试在该方法上使用Robert Penner 的缓动方程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

4 回答 4

5

好的,让我们对此进行一些数学运算

我一直是并倡导数学在游戏开发中的重要性和实用性,也许我在这个答案上走得太远了,但我真的认为你的问题根本不是关于编码,而是关于建模和解决代数问题. 不管怎样,我们走吧。

参数化

如果你有大学学位,你可能会记得一些关于函数的东西——接受一个参数并产生一个结果的操作——和图形——一个函数与它的参数演变的图形表示(或绘图)。f(x)可能会提醒你一些事情:它说一个名为的函数f取决于参数x。因此,“参数化”大致意味着用一个或多个参数来表达一个系统。

您可能不熟悉这些术语,但您一直都在这样做。Track例如,您的 是一个具有 3 个参数的系统: f(x,y,z).

关于参数化的一个有趣的事情是你可以抓取一个系统并用其他参数来描述它。同样,您已经在这样做了。当您描述轨迹随时间的演变时,您是在说每个坐标都是时间的函数,f(x,y,z) = f(x(t),y(t),z(t)) = f(t). 换句话说,您可以使用时间来计算每个坐标,并使用坐标在给定时间内将您的对象定位在空间中。

为轨道系统建模

最后,我将开始回答你的问题。为了完整地描述您想要的 Track 系统,您将需要两件事:

  1. 路径;

你实际上已经解决了这部分。您在场景空间中设置了一些点,并使用Catmull-Rom 样条线对这些点进行插值并生成路径。这很聪明,没有什么可做的了。

此外,您在每个点上添加了一个字段time,因此您希望确保移动对象将在这个确切时间通过此检查。我稍后会回来讨论这个。

  1. 一个移动的物体。

关于您的路径解决方案的一件有趣的事情是您使用参数对路径计算进行了percentageThroughSegment参数化 - 一个范围从 0 到 1 的值,表示段内的相对位置。在您的代码中,您以固定的时间步长进行迭代,您percentageThroughSegment将得到所花费的时间与段的总时间跨度之间的比例。由于每个段都有特定的时间跨度,因此您可以模拟许多恒定速度。

这是相当标准的,但有一个微妙之处。您忽略了描述运动的一个非常重要的部分:行进的距离

我建议你采用不同的方法。使用经过的距离来参数化您的路径。然后,对象的运动将是相对于时间参数化的行进距离。这样,您将拥有两个独立且一致的系统。动手工作!

例子:

从现在开始,为了简单起见,我会将所有内容都设为 2D,但稍后将其更改为 3D 将是微不足道的。

考虑以下路径:

示例路径

哪里i是段的索引,d是行进的距离,x, y是平面内的坐标。这可能是由像您这样的样条曲线或贝塞尔曲线或其他任何东西创建的路径。

使用当前解决方案的对象产生的运动可以描述为distance traveled on the pathvs的图表,time如下所示:

运动1图

表中的哪里t是物体必须到达检查点的时间,d又是到达该位置的距离,v是速度,a是加速度。

上图显示了物体如何随时间前进。横轴是时间,纵轴是经过的距离。我们可以想象,纵轴是一条“展开”成一条直线的路径。下图是速度随时间的演变。

在这一点上,我们必须回忆一些物理学,并注意,在每一段,距离图是一条直线,对应于匀速运动,没有加速度。这样的系统由以下等式描述:d = do + v*t

运动 1 动画

每当物体到达检查点时,它的速度值就会突然改变(因为它的图表中没有连续性),这在场景中产生了奇怪的效果。是的,您已经知道了,这正是您发布问题的原因。

好的,我们怎样才能让它变得更好?嗯……如果速度图是连续的,那就不会是那种烦人的速度跳跃了。像这样的运动最简单的描述可能是均匀加速。这样的系统由以下等式描述:d = do + vo*t + a*t^2/2。我们还必须假设一个初始速度,我将在这里选择零(与休息分开)。

在此处输入图像描述

正如我们预期的那样,速度图是连续的,运动通过路径加速。这可以编码到 Unity 中更改方法StartGetPosition如下所示:

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);
}

好吧,让我们看看情况如何......

在此处输入图像描述

呃……呃-哦。它看起来几乎不错,除了在某些时候它向后移动然后再次前进。实际上,如果我们检查我们的图表,就会在其中进行描述。在 12 到 16 秒之间,速度是负的。为什么会这样?因为这种运动功能(恒定加速度)虽然简单,但也有一些局限性。对于一些突然的速度变化,可能没有一个恒定的加速度值可以保证我们的前提(在正确的时间通过检查点)而不会产生类似的副作用。

我们现在干什么?

你有很多选择:

  • 描述一个具有线性加速度变化的系统并应用边界条件(警告:要解决很多方程);
  • 描述一个在一段时间内具有恒定加速度的系统,例如在曲线之前/之后加速或减速,然后在其余部分保持恒定速度(警告:要解决的方程更多 ,很难保证正确通过检查点的前提时间);
  • 使用插值方法生成随时间变化的位置图。我已经尝试过 Catmull-Rom 本身,但我不喜欢结果,因为速度看起来不是很流畅。贝塞尔曲线似乎是一种更可取的方法,因为您可以直接操纵控制点上的斜率(又称速度)并避免向后移动;
  • 我最喜欢的是:AnimationCurve在类中添加一个公共字段,并在编辑器中使用 ts 超棒的内置抽屉自定义您的运动图!您可以使用其方法轻松添加控制点,并使用其AddKey方法获取一段时间的位置Evaluate。当您在曲线中编辑场景时,您甚至可以使用OnValidate组件类上的方法自动更新场景中的点,反之亦然。

不要停在那里!在路径的线 Gizmo 上添加渐变以轻松查看它更快或更慢的位置,添加用于在编辑器模式下操纵路径的句柄......发挥创意!

于 2018-06-15T12:06:28.543 回答
0

据我所知,您已经有了大部分解决方案,只是初始化不正确。

局部速度取决于样条线的长度,因此您应该通过线段长度的倒数来调制速度(您可以通过几步轻松近似)。

当然,在您的情况下,您无法控制速度,只能控制输入时间,因此您需要SimpleWayPoint.time 根据先前样条线段的顺序和长度正确分配 的值,而不是在字段中手动初始化它声明。这种方式percentageThroughSegment应该是均匀分布的。

正如评论中提到的,其中一些数学看起来可能更简单Lerp():)

于 2018-06-11T16:28:28.437 回答
0

让我们先定义一些术语:

  1. t: 每个样条的插值变量,范围从01
  2. s:每个样条的长度。根据您使用的样条类型(catmull-rom、bezier 等),有一些公式可以计算估计的总长度。
  3. dtt每帧的变化。在您的情况下,如果这在所有不同的样条曲线上都是恒定的,您将在样条曲线端点看到突然的速度变化,因为每个样条曲线具有不同的长度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()
  • 考虑在第二个样条线的开始处实施“缓入”
于 2018-06-12T03:18:58.707 回答
0

您可以尝试使用他们为车轮系统提供的 Wheelcollider 教程。

它有一些变量可以与刚体变量一起调整以实现模拟驾驶。

当他们写

一个车辆实例上最多可以有 20 个车轮,每个车轮都施加转向、电机或制动扭矩。

免责声明:我使用 WheelColliders 的经验很少。但在我看来,它们就像你要找的东西。

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

在此处输入图像描述

于 2018-06-12T20:08:29.880 回答