好吧,让我们放一些math关于这一点。
我一直主张数学在游戏开发中的重要性和实用性,也许我在这个答案上讨论得太过分了,但我真的认为你的问题根本不是关于编码的,而是关于建模和解决代数问题的。不管怎样,我们走吧。
参数化
如果你有大学学位,你可能会记得一些关于功能- 接受参数并产生结果的操作 - 以及graphs- 函数与其参数的演变的图形表示(或绘图)。f(x)
可能会提醒你一件事:它说一个名为f
取决于普拉特x
。所以,“为了参数化 https://en.wikipedia.org/wiki/Parametrization“粗略地表示用一个或多个参数来表达一个系统。
您可能不熟悉这些术语,但您一直在这样做。你的Track
例如,是一个具有 3 个参数的系统:f(x,y,z)
.
关于参数化的一个有趣的事情是,您可以获取一个系统并用其他参数来描述它。再说一次,你已经在这样做了。当你描述轨迹随时间的演变时,你是说每个坐标都是时间的函数,f(x,y,z) = f(x(t),y(t),z(t)) = f(t)
。换句话说,您可以使用时间来计算每个坐标,并使用这些坐标在给定时间内将对象定位在空间中。
轨道系统建模
最后,我开始回答你的问题。为了完整地描述您想要的 Track 系统,您需要两件事:
- A path;
您实际上已经解决了这部分。您在场景空间中设置一些点并使用Catmull–Rom 样条插值点并生成路径。这很聪明,而且没有什么可做的。
另外,您还添加了一个字段time
在每个点上,因此您要确保移动对象将在此时通过此检查。我稍后会回来讨论这个问题。
- 一个移动的物体。
关于 Path 解决方案的一件有趣的事情是,您使用一个参数化了路径计算percentageThroughSegment
参数 - 范围从 0 到 1 的值,表示段内的相对位置。在您的代码中,您以固定的时间步长进行迭代,并且您的percentageThroughSegment
将是所花费的时间与该段的总时间跨度之间的比例。由于每个段都有特定的时间跨度,因此您可以模拟许多恒定速度。
这是相当标准的,但有一个微妙之处。您忽略了描述运动的一个非常重要的部分:行驶距离.
我建议你采用不同的方法。使用行驶的距离来参数化您的路径。那么,物体的运动将是相对于时间参数化的行进距离。这样您将拥有两个独立且一致的系统。动手干活!
Example:
从现在开始,为了简单起见,我会将所有内容都设为 2D,但稍后将其更改为 3D 将是微不足道的。
考虑以下路径:
Where i
是段的索引,d
是行驶的距离,并且x, y
是平面上的坐标。这可能是由像您这样的样条曲线创建的路径,或者使用贝塞尔曲线或其他曲线创建的路径。
使用当前解决方案的对象所产生的运动可以描述为以下图表:distance traveled on the path
vs time
像这样:
Where t
表中是物体必须到达检查点的时间,d
又是到达该位置的距离,v
是速度并且a
是加速度。
上部显示了物体如何随时间前进。横轴是时间,纵轴是行驶的距离。我们可以想象垂直轴是一条直线“展开”的路径。下图是速度随时间的演变。
此时我们必须回顾一些物理学,并注意,在每个段,距离图都是一条直线,对应于匀速运动,没有加速度。这样的系统由以下方程描述:d = do + v*t
每当对象到达检查点时,其速度值就会突然发生变化(因为其图形中没有连续性),这会在场景中产生奇怪的效果。是的,您已经知道这一点,这正是您提出这个问题的原因。
好吧,我们怎样才能做得更好呢?嗯...如果速度图是连续的,就不会出现令人讨厌的速度跳跃。对这样的运动最简单的描述可以是均匀加速。这样的系统由以下方程描述:d = do + vo*t + a*t^2/2
。我们还必须假设一个初始速度,我将在这里选择零(与静止分开)。
正如我们所期望的,速度图是连续的,运动通过路径加速。可以将其编码到 Unity 中,更改方法Start
and 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);
}
好吧,让我们看看进展如何……
呃……呃哦。
它看起来几乎不错,只是在某些时候它会向后移动,然后再次前进。实际上,如果我们检查图表,就会发现那里有描述。在 12 到 16 秒之间,速度为负值。为什么会出现这种情况?因为这种运动功能(恒定加速度)虽然简单,但有一些局限性。对于一些突然的速度变化,可能没有一个恒定的加速度值可以保证我们的前提(在正确的时间通过检查点)而不会产生类似的副作用。
我们现在干什么?
你有很多选择:
- 描述具有线性加速度变化的系统并应用边界条件(警告:lots需要求解的方程组);
- 描述一个在一段时间内保持恒定加速度的系统,例如在曲线之前/之后加速或减速,然后在路段的其余部分保持恒定速度(警告:更方程求解,很难保证在正确的时间通过检查点的前提);
- 使用插值方法生成随时间变化的位置图。我尝试过 Catmull-Rom 本身,但我不喜欢结果,因为速度看起来不太流畅。贝塞尔曲线似乎是一种更好的方法,因为您可以直接操纵控制点上的斜率(又称速度)并避免向后移动;
- 我最喜欢的:添加公共
AnimationCurve
类上的字段并在编辑器中使用超棒的内置抽屉自定义您的运动图表!您可以使用其轻松添加控制点AddKey
方法并用其获取一段时间的位置Evaluate
方法。
你甚至可以使用OnValidate
组件类上的方法可以在您在曲线中编辑场景时自动更新场景中的点,反之亦然。
不要停在那里!在路径的线条 Gizmo 上添加渐变,以轻松查看其速度更快或更慢的位置,添加用于在编辑器模式下操作路径的手柄...发挥创意!