Search Unity

Use a PID loop to control Unity game objects

Discussion in 'Scripting' started by JoeStrout, Apr 5, 2016.

  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I just wrote up a blog post showing how to use a PID controller in Unity.

    I needed this today because I'm making a controller for a hovercraft, and there are so many layers (physics, control response time, etc.) between the virtual controls the AI should be manipulating, and the actual position/speed of the craft, that constructing a controller heuristically would have been quite a pain. PID loop to the rescue! I just threw this in, spent a few minutes tweaking the three constants, and now I have a script that can fly my hovercraft to any desired altitude better than I can do it myself.



    So, I thought I'd share. I hope somebody finds it useful!
     
    nidorx, Rujash, Kiwasi and 1 other person like this.
  2. Steve-Tack

    Steve-Tack

    Joined:
    Mar 12, 2013
    Posts:
    1,240
    Interesting timing. I just heard the term "PID controller" yesterday for the first time during a discussion about space combat AI. Apparently you can use those to help control rigidbody physics (applying linear forces and toque to get desired behavior without "cheating" by directly changing position or rotation).

    When you get into things like wanting an AI spaceship to follow a spline path, etc, it can get tricky.

    In my space game, I use rigidbody linear forces for AI ship movement, but when it's time for accurate aiming, I cheat and do direct rotations. It's OK, but if I need to get into more precise AI control, I guess a PID controller would help?

    It's also interesting that the example is a hover vehicle. I recently prototyped a flying vehicle that you can toggle between VTOL/hover mode and more of a jet plane mode. Getting the auto-hover to work was a little fiddly, but I got it working. There's a fair range of vertical motion during auto-hover - there's a delay between when it detects a negative Y velocity and the effect of increasing throttle to actually start ascending. Though visually that may actually be more interesting than a more accurate solution, though maybe a PID controller would have made it easier.

    It might be cool to see the PoliceCarMotion class to see how the rotor speed is actually being used.

    Anyway, it looks intriguing, thanks for the post!
     
  3. steego

    steego

    Joined:
    Jul 15, 2010
    Posts:
    969
    Looks great, ideally though you'd also want to clamp the integer term to make sure it doesn't run away / overflow. Not a common occurrence, but can happen if the system uses a long time to reach its target.
     
  4. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that's the idea exactly. The relationship between the forces you apply and the motion of the object can be pretty complex... a properly tuned PID controller can let you ignore the details, and just apply a force.

    That's the next step for my police car, as it happens. My plan is to have a "rabbit" which simply moves along the spline path, maintaining a distance of at least N meters from the vehicle; and then have the vehicle controller chase that rabbit, by using the PID controller above to get the correct attitude, another one to point in the right direction, and then just accelerate in proportion to how far away it is in X and Z (within limits).

    Ah, well there I am cheating a bit. It's a very simplified linear physics model with velocity and drag, with some cheap hacks to combine upward, forward, and sideways (strafing) force. And there's no angular momentum. It just reads rotorSpeed, forward (thrust), strafe (sideways thrust) and turn rate from the model, and then does this:

    Code (CSharp):
    1.     void Update() {      
    2.         // apply gravity (relative to an earth-normal acceleration of about 9 m/sec)
    3.         float thrust = maxThrust * model.rotorSpeed.value * model.rotorSpeed.value;
    4.         velocity += gravity * 9 * Time.deltaTime;
    5.        
    6.         // apply thrust, taking into account forward and strafing behavior
    7.         Vector3 direction = -gravity.normalized
    8.             + transform.forward * model.forward.value
    9.             + transform.right * model.strafe.value;
    10.         direction.Normalize();
    11.        
    12.         velocity += direction * thrust * 9 * Time.deltaTime;
    13.        
    14.         // apply drag
    15.         velocity -= velocity.normalized * velocity.sqrMagnitude * drag;
    16.        
    17.         // update position
    18.         transform.position += velocity * Time.deltaTime;
    19.        
    20.         // apply a floor
    21.         if (transform.position.y < minY) {
    22.             transform.position = new Vector3(transform.position.x, minY, transform.position.z);
    23.             velocity.y = 0;
    24.         }
    25.        
    26.         // rotate
    27.         if (model.rightTurn.value != 0) {
    28.             transform.RotateAround(transform.position, -gravity, model.rightTurn.value * turnRate * Time.deltaTime);
    29.         }
    30.     }
     
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    That's a good idea. This sort of thing does happen, for example when I tell it to go to Y=10, but it can't actually drop that far because it's already on the floor (er, roof). The I term will just keep getting bigger and bigger.

    The other thing I'll probably add though is a "Reset" method that just clears the I term (and maybe somehow clears lastError too). You would call this when you're suddenly changing the target substantially, since any accumulated error up to that point probably doesn't matter anymore.
     
  7. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
  8. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    I really should start using PIDs in Unity. I use them all the time in my day job. They are pretty nifty for 'unknown model' situations.

    In my first role we had kit so old that it relied on mechanical PIDs. I bent one of the springs while trying to change the set point. It shut down the plant for about an hour while the technician readjusted it. Good times.

    You can get some fun effects when you chain the output of one PID to the set point of another.

    I should look up my old control systems text book on tuning. The process is pretty straight forward, to the point that you can auto tune PIDs. I remember doing it by hand pretty quickly. It would be especially simple in a video game where it doesn't matter if the process becomes unstable and blows up. Can't blow thing up at work. That tends to upset the neighbours.
     
    brainwipe, wickedwaring and JoeStrout like this.
  9. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
    After the stuff I actually posted, I was chasing exactly that. It's pretty tricky to figure out how to mix all those factors together. Then I got sidetracked fighting a couple other problems, like deciding when I was 180 degrees out from the target heading and how to loop around to deal with that ... and I sort of lost interest. :)

    Reset is a must-have, I'm surprised few PID implementations have it...
     
  10. MV10

    MV10

    Joined:
    Nov 6, 2015
    Posts:
    1,889
    My original intent with the little PID test thing I posted was to use it for auto-tune, but it ends up being so easy to use sliders to tweak the values that it didn't seem worth the effort to build all that extra loop/analysis stuff.
     
  11. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Just don't call it reset or you will confuse poor old hands like me. Reset refers to 1/Ki. It's an alternative way to describe intergral.

    I'd also suggest making it true output tracking. Instead of setting to zero, let it set your output to any value you are interested in to start control from there. It's a useful technique for kick starting slow controllers.
     
    JoeStrout and MV10 like this.
  12. Steve-Tack

    Steve-Tack

    Joined:
    Mar 12, 2013
    Posts:
    1,240
    Finally got around to trying a minimal PID controller example to have a 2D spaceship maintain a target velocity. Pretty cool! I can see it will be useful in a few different scenarios.

    Here's the WebGL version (might have to wait a few seconds for it to load):
    http://www.stevetack.com/unity/PIDController/Test1/index.html

    If you hit "2", let it come up to speed, then hit "0", you can see it reverse thrust. Turning the drag off shows what those PID settings would do in a more realistic space game.

    I found a "classic" 2010 post on PID controllers that's just as relevant as when it was first posted, and the Unity example still works fine:
    http://forum.unity3d.com/threads/pid-controller.68390/

    The only real difference with that PID controller is that you pass it the target value (setpoint) and actual value, and it subtracts the difference in its Update:

    Code (csharp):
    1. public float Update(float setpoint, float actual, float timeFrame) {
    2.         float present = setpoint - actual;
    3.         integral += present * timeFrame;
    4.         float deriv = (present - lastError) / timeFrame;
    5.         lastError = present;
    6.         return present * pFactor + integral * iFactor + deriv * dFactor;
    7.     }
    Anyway, the one question I have has to do with clamping the result from Update to 0-1. It looks like when not clamping the output value, it's typically "around" 0 to 1. When would it be appropriate to clamp? I'm thinking even in my example it may have made sense to avoid weird spikes, but I'm a little fuzzy on that. What I had to do in my example is multiply the output for use as the force value in AddRelativeForce to get it to act right.
     
    JoeStrout likes this.
  13. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    PID's are awesome, especially when they're used in a controlled environment. I'm guessing you're running tests in such an environment and hence all appears stable and within limits. This may not always be the case however. The integral accumulates an error, which if not resolved, will throw your values out of range. This happens when the PID is unable to reach it's target - like when your spaceship gets stuck between two objects for example.

    The solution is to implement what's known as "anti-windup" - essentially clamping the integral to a sensible value...

    Code (CSharp):
    1.  integral = clamp(integral + present * timeFrame, intLimit);
     
    JoeStrout likes this.