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:
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 ![]()