团结加速

时间:2018-06-06 20:29:40

标签: c# unity3d interpolation catmull-rom-curve

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

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

Cube mid way through Catmull-Rom Spline

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

为了尝试解决此问题,我尝试在GetTime(Vector3 p0, Vector3 p1, float alpha)方法上使用Robert Penner's easing equations。然而,虽然这确实有所帮助,但这还不够。在转换之间仍然存在速度跳跃。

有没有人对如何动态缓解立方体的位置以使其看起来像加速和减速,而没有轨道各段之间的大幅度跳跃有任何想法?

我编写了一个脚本,显示了我的代码的简单实现。它可以附加到任何游戏对象。为了便于查看代码运行时发生的情况,请附加到多维数据集或球体之类的内容。

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

3 个答案:

答案 0 :(得分:0)

据我所知,你已经掌握了大部分解决方案,只是初步化不当。

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

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

正如评论中所提到的,使用Lerp()时,某些数学运算看起来更简单:)

答案 1 :(得分:0)

让我们先定义一些术语:

  1. t:每个样条线的插值变量,范围从01
  2. s:每个样条曲线的长度。根据您使用的样条曲线类型(catmull-rom,bezier等),有计算估计总长度的公式。
  3. dt:每帧t的变化。在您的情况下,如果这在所有不同样条曲线上保持不变,您将看到样条曲线终点处的突然速度变化,因为每个样条曲线具有不同的长度s
  4. 缓解每个关节速度变化的最简单方法是:

    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()中使用非线性插值函数。
    • 考虑实施&#34; easy-into&#34;也是在第二个样条的开头

答案 2 :(得分:0)

您可以尝试使用他们用于轮系统的wheelcollider教程。

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

他们写作

  

单个车辆上最多可以有20个车轮,每个车轮都应用转向,电机或制动扭矩。

免责声明:我对使用WheelColliders的经验很少。但它们看起来就像你在寻找我。

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

enter image description here