本文介绍了Unity加速的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在Unity中模拟加速和减速.

I am trying to emulate acceleration and deceleration in Unity.

我已经写了一些代码来在Unity中生成轨道,并根据时间将对象放置在轨道上的特定位置.结果看起来像这样.

I have written to code to generate a track in Unity and place an object at a specific location on the track based on time. The result looks a little like this.

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

The issue I currently have is that each section of the spline is a different length and the cube moves across each section at a different, but uniform, speed. This causes there to be sudden jumps in the change of the speed of the cube when transitioning between sections.

为了尝试解决此问题,我尝试在 Robert Penner的缓和方程上使用GetTime(Vector3 p0, Vector3 p1, float alpha)方法.然而,尽管这确实有所帮助,但这还不够.在转换之间,速度仍然有跳跃.

In order to try and fix this issue, I attempted to use Robert Penner's easing equations on the GetTime(Vector3 p0, Vector3 p1, float alpha) method. However, whilst this did help somewhat, it was not sufficient. There were still jumps in speed in between transitions.

有人对我如何动态地简化立方体的位置以使其看起来像在加速和减速而在轨道的各段之间没有大的速度变化有任何想法吗?

Does anyone have any ideas on how I could dynamically ease the position of the cube to make it look like it was accelerating and decelerating, without large jumps in speed between segments of the track?

我编写了一个脚本,该脚本显示了我的代码的简单实现.它可以附加到任何游戏对象上.为了便于查看代码运行时发生的情况,请附加到诸如立方体或球体之类的东西.

I have written a script that shows a simple implementation of my code. It can be attached to any game object. To make it easy to see what is happening when the code runs, attach to something like a cube or sphere.

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

推荐答案

好的,让我们在上面加上一些数学.

Ok, let's put some math on this.

我一直以来都提倡数学在gamedev中的重要性和实用性,也许我在这个答案上太过分了,但是我真的认为您的问题根本不是关于编码,而是关于建模和求解代数问题.无论如何,我们走吧.

I've always been and advocate of the importance and utility of math in gamedev, and maybe I go too far into this on this answer, but I really think your question is not about coding at all, but about modelling and solving an algebra problem. Anyway, let´s go.

如果您拥有大学学历,您可能会记得有关功能的一些知识-函数带有参数并产生结果-和 graphs -图形表示(或绘图) )函数的演化与其参数的关系. f(x)可能会提醒您一些事情:它说一个名为f的函数取决于参数x.因此,"参数化"大致意味着用一个或多个参数来表示一个系统.

If you have a college degree, you may remember something about functions - operations that take a parameter and yield a result - and graphs - a graphic representation (or plot) of the evolution of a function vs. its parameter. f(x) may remind you something: it says that a function named f depends on the prameter x. So, "to parameterize" roughly means expressing a system it in terms of one or more parameters.

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

You may not be familiarized with the terms, but you do it all the time. Your Track, for example, is a system with 3 parameters: f(x,y,z).

关于参数化的一件有趣的事情是,您可以获取一个系统并根据其他参数对其进行描述.同样,您已经在执行此操作.当您描述轨迹随时间的演变时,您会说每个坐标都是时间的函数,f(x,y,z) = f(x(t),y(t),z(t)) = f(t).换句话说,您可以使用时间来计算每个坐标,并使用该坐标在给定时间内将对象放置在空间中.

One interesting thing about parameterization is that you can grab a system and describe it in terms of other parameters. Again, you are already doing it. When you describe the evolution of your track with time, you are sayng that each coordinate is a function of time, f(x,y,z) = f(x(t),y(t),z(t)) = f(t). In other words, you can use time to calculate each coordinate, and use the coordinates to position your object in space for that given time.

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

Finally, I'll start answering your question. For describing completely the Track system you want, you will need two things:

  1. 路径;

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

You practically solved this part already. You set up some points in Scene space and use a Catmull–Rom spline to interpolate the points and generate a path. That is clever, and there is no much left to do about it.

此外,在每个点上都添加了一个字段time,以便确保移动的对象将在此准确时间通过此检查.我稍后会再讨论.

Also, you added a field time on each point so you want to asure the moving object will pass through this check at this exact time. I'll be back on this later.

  1. 运动物体.

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

One interesting thing about your Path solution is that you parameterized the path calculation with a percentageThroughSegment parameter - a value ranging from 0 to 1 representing the relative position inside the segment. In your code, you iterate at fixed time steps, and your percentageThroughSegment will be the proportion between the time spent and the total time span of the segment. As each segment have a specific time span, you emulate many constant speeds.

这很标准,但是有一个微妙之处.您忽略了描述运动的极其重要的部分:行进的距离.

That's pretty standard, but there is one subtlety. You are ignoring a hugely important part on describing a movement: the distance traveled.

我建议您使用其他方法.使用行进的距离来参数化您的路径.然后,对象的移动将是相对于时间参数化的行进距离.这样,您将拥有两个独立且一致的系统.努力工作!

I suggest you a different approach. Use the distance traveled to parameterize your path. Then, the object's movement will be the distance traveled parameterized with respect to time. This way you will have two independent and consistent systems. Hands to work!

从现在开始,为了简单起见,我将全部制作为2D,但是稍后将其更改为3D将很简单.

From now on, I'll make everything 2D for the sake of simplicity, but changing it to 3D later will be trivial.

请考虑以下路径:

其中i是线段的索引,d是行进的距离,x, y是平面中的坐标.这可能是一条由您的样条线创建的路径,或者是由Bézier曲线创建的路径.

Where i is the index of the segment, d is the distance traveled and x, y are the coords in plane. This could be a path created by a spline like yours, or with Bézier curves or whatever.

对象在当前解决方案下产生的运动可以描述为distance traveled on the path vs time的图形,如下所示:

The movement developed by a object with your current solution could be described as a graph of distance traveled on the path vs time like this:

表中的t是对象必须到达检查位置的时间,d还是到达该位置的距离,v是速度,而a是加速度.

Where t in the table is the time that the object must reach the check, d is again the distance traveled up to this position, v is the velocity and a is the acceleration.

上部显示对象如何随时间前进.横轴是时间,纵轴是行进距离.我们可以想象垂直轴是一条在一条直线上展开"的路径.下图是速度随时间的变化.

The upper shows how the object advances with time. The horizontal axis is the time and the vertical is the distance traveled. We can imagine that the vertical axis is the path "unrolled" in a flat line. The lower graph is the evolution of the speed over time.

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

We must recall some physics at this point and note that, at each segment, the graph of the distance is a straight line, that corresponds to a movement at constant speed, with no acceleration. Such a system is described by this equation: d = do + v*t

无论何时物体到达检查点,其速度值都会突然改变(因为其图形中没有连续性),这在场景中产生了怪异的效果.是的,您已经知道了,这就是您发布问题的原因.

Whenever the object reaches the check points, its speed value suddenly changes (as there is no continuity in its graph) and that has a weird effect in the scene. Yes, you already know that and that's precisely why you posted the question.

好的,我们怎样才能使它变得更好?嗯...如果速度图是连续的,那不会是令人讨厌的速度跳跃.对这种运动的最简单描述可以统一加速.这样的系统由以下等式描述:d = do + vo*t + a*t^2/2.我们还必须假设初始速度,在这里我将选择零(与静止分开).

Ok, how can we make that better? Hmm... if the speed graph were continuous, the wouldn't be that annoying speed jump. The simplest description of a movement like this could be an uniformly acelerated. Such a system is described by this equation: d = do + vo*t + a*t^2/2. We will also have to assume an initial velocity, I'll choose zero here (parting from rest).

就像我们期望的那样,速度图是连续的,运动是通过路径加速的.可以将其编码为Unity,从而更改方法StartGetPosition,如下所示:

Like we expected, The velocity graph is continuous, the movement is accelerated throug the path. This could be coded into Unity changing the methids Start and GetPosition like this:

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

好吧,让我们看看它的进展...

Ok, let's see how it goes...

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

Err... uh-oh.It looked almost good, except that at some point it move backwards and then advance again. Actually, if we check our graphs, it is described there. Between 12 and 16 sec the velocity is negatie. Why does this happen? Because this function of movement (constant accelerations), altough simple, have some limitations. With some abrupt velocity variations, there may not be a constant value of acceleration that can guarantee our premise (passing on checkpoints at correct time) without have side-effects like those.

我们现在该怎么办?

您有很多选择:

  • 描述一个具有线性加速度变化的系统并应用边界条件(警告:很多个要求解的方程式";
  • 描述在一段时间内具有恒定加速度的系统,例如在曲线之前/之后加速或减速,然后在其余部分保持恒定速度(警告:甚至更多方程式需要求解,难以保证在正确的时间通过检查点的前提);
  • 使用插值方法生成时间图.我已经尝试过Catmull-Rom本身,但是我不喜欢这个结果,因为速度看上去并不十分平稳.贝塞尔曲线似乎是一种更可取的方法,因为您可以直接在控制点上操纵坡度(即速度),并避免向后运动;
  • 还有我最喜欢的:在类上添加一个公共AnimationCurve字段,并使用ts很棒的内置抽屉在Editor中自定义您的运动图!您可以使用AddKey方法轻松添加控制点,并使用Evaluate方法轻松获取位置一段时间.您甚至可以在组件类上使用OnValidate方法,以便在曲线中编辑场景时自动更新场景中的点,反之亦然.
  • Describe a system with linear acceleration changes and apply boundary conditions (Warning: lots of equations to solve);
  • Describe a system with constant acceleraction for some time period, like accelerate or decelerate just before/after curve, then keep constant speed for the rest of the segment (Warning: even more equations to solve, hard to guarantee the premise of passing checkpoints in correct time);
  • Use an interpolation method to generate a graph of position through time. I've tried Catmull-Rom itself, but I didn't like the result, because the speed didn't look very smooth. Bezier Curves seem to be a preferable approach because you can manipulate the slopes (aka speeds) on the control points directally and avoid backward movements;
  • And my favourite: add a public AnimationCurve field on the class and customize your movement graph in Editor with ts awesome built-in drawer! You can easily add control points with its AddKey method and fetch position for a time with its Evaluate method.You could even use OnValidate method on your component class to automatically update the points in the Scene when you edit it in the curve and vice-versa.

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

Don't stop there! Add a gradient on the path's line Gizmo to easily see where it goes faster or slower, add handles for manipulating path in editor mode... get creative!

这篇关于Unity加速的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

06-20 14:23