Spaceship control using PID controllers

I wanted to share a possible approach with everyone. Working on my hobby space sim lead me to issues of controlling my spaceship in a Newtonian environment. It’s absurdly difficult to keep the nose pointing where you want it to when you have to counter-thrust every motion. So I did a little research, and implemented a script based on Maker16’s excellent work - can be seen here:

http://forum.unity3d.com/threads/52280-Need-help-with-torque-rotations-and-spacecraft-control/page2?p=333928&viewfull=1#post333928

It was definitely a lot closer to what I wanted, even though the jitter fix seemed a bit of a cludge (worked great though) However, a controller like this can’t react to “steady” errors, and I wanted something that would give the pilot a little more help dealing with outside forces. I don’t have too many relevant things in my mental toolbox - but I have had some experience with controlling things in the real world, where your controller might have to account for friction, device degradation, changes in the environment, etc without a total picture of what’s going on.

It seemed like a pretty typical “cruise control” problem to me - a cruise control can’t be upset by changes in elevation, engine efficiency, grade of road, air resistance, etc. It has to adjust to these things - and it doesn’t operate in a binary ‘on/off’ fashion either, nobody wants a jerky car on the freeway.

Systems like this are usually controlled by PID loops, and I was curious if this application was suitable for the same approach. It actually turned out to work quite well, and I quickly tuned it to values that settled quickly, without a need for a snap-threshold or anything else.

So here is my implementation of pitch/yaw/roll control in a Newtonian environment using a 3-axis PID controller.

using System;
using UnityEngine;
using System.Collections;

public class ShipControlsPid : MonoBehaviour {
   
    public GUIText ScreenReadout;
   
    public Vector3 thrust = new Vector3(1,1,1);        //Total thrust per axis
    private Vector3 targetVelocity;                    //user input determines how fast user wants ship to rotate
      public Vector3 torques;                            //the amount of torque available for each axis, based on thrust
    public float maxRate = 4;                        //max desired turn rate
    private Vector3 curVelocity;                    //holds the rigidbody.angularVelocity converted from world space to local
   
    public Vector3 Kp = new Vector3(4, 4, 4);
    public Vector3 Ki = new Vector3(.007f,.007f,.007f);
    public Vector3 Kd = new Vector3(0,0,0);
   
    private PidController3Axis pControl = new PidController3Axis();
   
    void Start() {
        //this is where the bounding box is used to create pseudo-realistic torque;  If you want more detail, just ask.
        var shipExtents = ((MeshFilter)GetComponentInChildren(typeof(MeshFilter))).mesh.bounds.extents;
        torques.x = new Vector2(shipExtents.y,shipExtents.z).magnitude*thrust.x;
        torques.y = new Vector2(shipExtents.x,shipExtents.z).magnitude*thrust.y;    //normally would be x and z, but mesh is rotated 90 degrees in mine. 
        torques.z = new Vector2(shipExtents.x,shipExtents.y).magnitude*thrust.z;    //normally would be x and y, but mesh is rotated 90 degrees in mine.
       
        ApplyValues();
    }
   
    void ApplyValues(){
        pControl.Kp = Kp;
        pControl.Ki = Ki;
        pControl.Kd = Kd;
        pControl.outputMax = torques;
        pControl.outputMin = torques * -1;
        pControl.SetBounds();
    }
   
    void RCS() {
        // Uncomment to catch inspector changes
        //ApplyValues();
       
        // collect inputs
        var rollInput = Input.GetAxisRaw("Roll");
        var pitchInput = Input.GetAxisRaw("Pitch");
        var yawInput = Input.GetAxisRaw("Yaw");
       
        //angular acceleration = torque/mass
        //var rates = torques/rigidbody.mass;
       
        //determine targer rates of rotation based on user input as a percentage of the maximum angular velocity
        targetVelocity = new Vector3(pitchInput*maxRate,yawInput*maxRate,rollInput*maxRate);
       
        //take the rigidbody.angularVelocity and convert it to local space; we need this for comparison to target rotation velocities
        curVelocity = transform.InverseTransformDirection(rigidbody.angularVelocity);               
       
        // run the controller
        pControl.Cycle(curVelocity, targetVelocity, Time.fixedDeltaTime);
        rigidbody.AddRelativeTorque(pControl.output * Time.fixedDeltaTime, ForceMode.Impulse);
       
        if (ScreenReadout == null) return;
        ScreenReadout.text = "Current V : " + curVelocity + "\n"
            + "Target V :" + pControl.output + "\n"
                + "Current T : " + tActivation + "\n";
       
    }
   
    void FixedUpdate() {
        RCS();
    }
   
}

public class PidController3Axis {
   
    public Vector3 Kp;
    public Vector3 Ki;
    public Vector3 Kd;
   
    public Vector3 outputMax;
    public Vector3 outputMin;

        public Vector3 preError;
   
    public Vector3 integral;
    public Vector3 integralMax;
    public Vector3 integralMin;
   
    public Vector3 output;
   
    public void SetBounds(){
        integralMax = Divide(outputMax, Ki);
        integralMin = Divide(outputMin, Ki);       
    }
   
    public Vector3 Divide(Vector3 a, Vector3 b){
        Func<float, float> inv = (n) => 1/(n != 0? n : 1);
        var iVec = new Vector3(inv(b.x), inv(b.x), inv(b.z));
        return Vector3.Scale (a, iVec);
    }
   
    public Vector3 MinMax(Vector3 min, Vector3 max, Vector3 val){
        return Vector3.Min(Vector3.Max(min, val), max);
    }
   
    public Vector3 Cycle(Vector3 PV, Vector3 setpoint, float Dt){
        var error = setpoint - PV;
        integral = MinMax(integralMin, integralMax, integral + (error * Dt));
       
        var derivative = (error - preError) / Dt;
        output = Vector3.Scale(Kp,error) + Vector3.Scale(Ki,integral) + Vector3.Scale(Kd,derivative);
        output = MinMax(outputMin, outputMax, output);
       
        preError = error;
        return output;
    }
}

For more background on PID controllers and examples of how to tune them you can read this excellent article:
http://tec.upc.es/mp/2012/PID-without-a-PhD.pdf

The PID loop I implemented is pretty standard, though it uses the technique for limiting integrator windup described here:
http://brettbeauregard.com/blog/2011/04/improving-the-beginner’s-pid-reset-windup/

Anyway, this seems to be working pretty well for me, I’m going to see about adding another PID controller to deal with the other 3 axii of motion, and also was curious to see what everyone thought.

Hmm, after I wrote this I realized that I never actually looked around Unity for pid stuff. Here are a couple of other threads on the subject, but another implementation won’t hurt anyone either :slight_smile:

http://forum.unity3d.com/threads/68390-PID-controller

2 Likes

This is incredibly helpful to me. I wonder if you could explain to me the best way to set up the kp, ki and kd values to get a desired result as I’m not sure how they affect the outcome. I also notice that sometimes my ship never comes to a complete stop so I assume it has something to do with those values needing changing.

I’m glad you found it helpful, sorry it took me a few days to see this. Yes, those values have to be tuned individually, and will work differently depending on how the object behaves in the unity physics system. Mesh size, rigid body mass, drag, etc - all have an effect. That said, it gets pretty easy to do a quick and dirty tuning, and there are a few methods for automated tuning as well that can probably be implemented. PID technology predates electronics, and the principles are well understood.

P factor is proportional - (compared to error - error being the difference between the desired value and current measurment). It’s the most “direct” value that affects things. The output depends on the error, big error, big output. Little error, little output. It’s just a multiplier. In the perfect world it would be enough, but it can’t react to steady accumulated errors (like friction, gravity, etc)

I factor is integrative - it adds up all the errors. This is what can be effective at controlling those accumulated errors, but if you set it too high you’ll get lots of overshot, etc. This one can get away from you, and there are several ways of controlling “integrator windup” - I cap the integrator sum in this implementation.

D factor is derivative - it changes the output is based on how fast the error is changing.

You should really read the wiki article on PID - they go through the theory and a few common loop tuning methods. It’s not that long, but really helpful - and if you’re going to use these (and PID is a very powerful, adaptable, low code/lightweight technique for these sorts of problems), you really need to understand the basics. I’m sorry if my code is a bit opaque - it’s really doing three separate PID loops with vector values, so looking up some pseudocode might help make things easier to descipher.

http://en.wikipedia.org/wiki/PID_controller

After that check out this article, and play with the program:

http://www.codeproject.com/Articles/36459/PID-process-control-a-Cruise-Control-example

That should get you started, if you hit a wall let me know, I’ll see what I can do.

Edit: Towards the end of page 5 of the PID without a PHD article linked in the OP, there is a short description of a rough tuning procedure that should get you a functioning (if not perfectly tuned) controller very quickly.

Thx your script has help me to make my drone physics script. Actullaly i was missing to do transform.InverseTransformDirection :slight_smile: