Use a PID loop to control Unity game objects

One of the most general and common tricks ever to come out of industrial control theory is the proportional–integral–derivative controller (PID controller), sometimes known as a "PID loop."  It is a simple equation that you can use to control any one-dimensional variable, such as a throttle, a motorized hinge, etc.  All it needs for an input is an "error" signal — that is, a measure of how far off something is from where you want it to be, plus three constants.  Most importantly, it does not need any model of how the value it's controlling relates to the output.

In this post, I'll give you well-encapsulated code for a PID controller in C#, and show how to use this in Unity to control the throttle of a hovercar.  The hovercar physics involves momentum, drag, and controls that don't respond instantly, just like a real hovercar!  But the PID controller doesn't care about any of that; once you have the three constants tuned, and hook up the error signal (in this case, the difference between the current altitude and the altitude you want), it does the rest.

First, here's the code for a PID controller, as a conveniently serializable class.

public class PIDController {
	[Tooltip("Proportional constant (counters current error)")]
	public float Kp = 0.2f;
	[Tooltip("Integral constant (counters cumulated error)")]
	public float Ki = 0.05f;
	[Tooltip("Derivative constant (fights oscillation)")]
	public float Kd = 1f;
	[Tooltip("Current control value")]
	public float value = 0;
	private float lastError;
	private float integral;
	/// Update our value, based on the given error.  We assume here that the
	/// last update was Time.deltaTime seconds ago.
	/// <param name="error" />Difference between current and desired outcome.
	/// Updated control value.
	public float Update(float error) {
		return Update(error, Time.deltaTime);
	/// Update our value, based on the given error, which was last updated
	/// dt seconds ago.
	/// <param name="error" />Difference between current and desired outcome.
	/// <param name="dt" />Time step.
	/// Updated control value.
	public float Update(float error, float dt) {
		float derivative = (error - lastError) / dt;
		integral += error * dt;
		lastError = error;
		value = Mathf.Clamp01(Kp * error + Ki * integral + Kd * derivative);
		return value;

There's nothing too complex here.  We have a couple of private variables to keep track of how the error is changing, and how much error has been integrated over recent time.  Then the update loop just implements the standard PID equation.

To use this, just make a public PIDController property in some Unity class.  Then call Update on it, with the difference between where whatever you're controlling is now, and where you want it to be.  In this example, we're controlling a hovecraft by setting the rotor speed, and measuring the altitude (transform Y value).

public class PoliceCarDriver : MonoBehaviour {
	public float targetAltitude = 20;
	public PIDController altitudePID;
	PoliceCarModel model;

	#region MonoBehaviour Events
	void Start() {
		model = GetComponentInChildren();
	void Update() {
		float curAltitude = transform.position.y;
		float err = targetAltitude - curAltitude; = Mathf.Clamp01(altitudePID.Update(err));

We also clamp the control value to the range 0-1, just because that's how our model controls are intended to work.  But the same PIDController class would work with values in any range; you might only need to tweak the three constants (kP, kI, and kD) until the behavior looks right.

The result is shown in the video below.


The main downside to a PID controller is that setting those three constants can be a bit fiddly, and if you get them wrong, you'll get bad behavior — overshoot, undershoot, oscillations, or just being too slow. This page has some useful graphs showing what incorrectly tuned constants look like.  With that in hand, I was able to get the behavior in the video above with only 5-10 minutes of tweaking the constants.

So the next time you have a Unity game object with complex behavior you need to control, consider a PID loop!  It may be just what you need.