Acceleration in Unity

后端 未结 4 1836
予麋鹿
予麋鹿 2021-02-20 04:38

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 trac

相关标签:
4条回答
  • 2021-02-20 05:29

    Let's define some terms first:

    1. t: interpolation variable for each spline, ranging from 0 to 1.
    2. 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.
    3. dt: the change in t 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 length s.

    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
    0 讨论(0)
  • 2021-02-20 05:33

    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:

    1. 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.

    1. 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 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.

    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!

    0 讨论(0)
  • 2021-02-20 05:33

    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() :)

    0 讨论(0)
  • 2021-02-20 05:36

    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

    0 讨论(0)
提交回复
热议问题