问题
I am trying to emulate acceleration and deceleration in 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.
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
}
}
回答1:
Ok, let's put some math on this.
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.
Parametrization
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.
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)
.
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.
Modelling a Track System
Finally, I'll start answering your question. For describing completely the Track system you want, you will need two things:
- A path;
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.
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.
- A moving object.
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!
Example:
From now on, I'll make everything 2D for the sake of simplicity, but changing it to 3D later will be trivial.
Consider the following path:
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.
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:
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.
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.
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).
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...
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.
What do we do now?
You have plenty of options:
- 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 itsAddKey
method and fetch position for a time with itsEvaluate
method. You could even useOnValidate
method on your component class to automatically update the points in the Scene when you edit it in the curve and vice-versa.
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!
回答2:
As far as I can tell you already have most of the solution in, just initialized improperly.
The local speed is dependent on the length of the spline, so you should modulate the speed by the inverse of the length of the segment (which you can easily approximate with a few steps).
Granted, in your case you don't have control over the speed, only the input time, so what you need is to distribute properly the values of SimpleWayPoint.time
according to the order and length of previous spline segments, instead of initializing it manually in the field declaration. This way percentageThroughSegment
should be evenly distributed.
As mentioned in comments, some of that math could look simpler with Lerp()
:)
回答3:
Let's define some terms first:
t
: interpolation variable for each spline, ranging from0
to1
.s
: the length of each spline. Depending on what type of spline you use (catmull-rom, bezier, etc.), there are formulas to calculate the estimated total length.dt
: the change int
per frame. In your case, if this is constant across all the different splines, you will see sudden speed change at spline end points, as each spline has a different lengths
.
The simplest way to ease the speed change at each joint is:
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
}
where:
float interpSpeed(float t, float v0, float v1, float tEaseStart=0.5f) {
float u = (t - tEaseStart)/(1f - tEaseStart);
return Mathf.Lerp(v0, v1, u);
}
The intuition above is that as I am reaching the end of my first spline, I predict the expected speed in the next spline, and ease my current speed to reach there.
Finally, in order to make the easing look even better:
- Consider using a non-linear interpolation function in
interpSpeed()
. - Consider implementing an "ease-into" also at the start of the second spline
回答4:
You could try work with the wheelcollider tutorial they have for their wheel system.
It has some variables you can adjust along with the Rigidbody variables to achieve simulated driving.
As they write
You can have up to 20 wheels on a single vehicle instance, with each of them applying steering, motor or braking torque.
Disclaimer: I have only minimal experience working with WheelColliders. But they seem like what you're looking for to me.
https://docs.unity3d.com/Manual/WheelColliderTutorial.html
来源:https://stackoverflow.com/questions/50729073/acceleration-in-unity