PID control pt. 1: Proportional Control

TL;DR: Section 2 for math, section 0 for teaser, sections 3, 4 for code, section 5 for examples :wink:

PID control pt. 1: Proportional Control (P)

Hi! This is part 1 of a tutorial series about PID controllers, what they are, how they can be used to control all sorts of things in games, and how to program them in Lua!
I want to keep it accessible and digestible, so Iā€™ve split the tutorial into these parts:

1. Proportional (P) Control *(this one :D)*
2. Derivative (D) Control
3. Integral (I) Control
4. PID, PI and PD Control
5. Tuning PID Control (maybe)

Right now only this part (pt. 1) is done, but Iā€™ll link to the other posts once Iā€™ve made them.

This first part covers proportional control which is the simplest to explain, understand and use. Each control type can be used on its own or in combination with the others, and many times you wonā€™t even need I (integral) or D (derivative) control. Iā€™ll also provide some example use cases of P controllers in this part. When we get to I and D control thereā€™s going to be a bit of calculus involved, but Iā€™ll try to explain that in as simple terms as possible. Donā€™t worry if you donā€™t get the math, in the end using a PID controller is all about tuning 3 numbers to get the kind of movement you want.

All of this control stuff comes from the world of industrial control systems, for controlling like oil refineries and other cool things. What Iā€™m presenting here is just a super simplified overview, but hopefully a good introduction to the topic. Thereā€™s sooo much more to know, and you can dedicate an entire career just to this stuff. It gets especially gnarly when it comes to tuning. Everything I know about tuning is really just intuition Iā€™ve gained from playing around with it :sweat_smile:

0. Teaser :eyes:

First up is a teaser of why P control is awesome! :smiley:

Hereā€™s a super simple ball that can be rolled to the right or left with the A and D keys.

The blue arrow shows a VectorForce and which direction itā€™s pushing the ball, and the blue bar shows the horizontal speed of the ball. It just applies a constant force of 300 units in one direction or the other, and as you can see in the clip that causes some issues. The ball is sluggish and difficult to control because it doesnā€™t accelerate or decelerate very fast, but at the same time we donā€™t have very good control over its top speed. It can almost keep accelerating until itā€™s going light speed, and it doesnā€™t stop by itself when I let go of the movement keys. This is where a P controller comes in! Check it out:

I added an orange bar that shows how much force is being applied, because with P control that isnā€™t constant. In the clip you can see that when the velocity of the ball is very different from the desired velocity, such as when starting from a standstill or changing directions, the force is very high. But as you approach the top speed (30 in this case) the force goes towards 0, which makes sense as we donā€™t want it to accelerate or decelerate very much since itā€™s already at the velocity we want. All of this makes the controls much more snappy, and we donā€™t get situations like before where I roll off the edge of the map because I didnā€™t have several seconds to decelerate. Itā€™s easy to configure - or tune - how snappy you want the controls, so if you want them to be super tight or a little bit floaty then thatā€™s all possible.

1. Feedback Control Loops :arrows_clockwise:

What are control loops and feedback, and why should we use them?

PID controllers are an example of feedback control loops. The control part comes from the fact that we use controllers to control things :sweat_smile:. The loop part means that signals - or information - goes in a loop from a controller to something thatā€™s being manipulated, which causes a change that is measured, and that measurement is sent back as another signal to the controller. Since the output from the controller affects the system, and the input to the controller is some measurement of the system, any output from the controller ends up being fed back to it, which is where we get the ā€œfeedbackā€ from. All of this is illustrated in this figure:

- Modified from this figure by Dougsim, CC BY-SA 4.0 https://creativecommons.org/licenses/by-sa/4.0, via Wikimedia Commons

Weā€™re going to use some of the terms and abbreviations from the figure throughout this series.

  • The Set Point (SP) is the state that we want the system to be in - in the rolling ball example SP is the desired velocity. This usually comes from outside the system, like a Control Script that figures out which direction the player wants to roll.

  • The Proces Value or Process Variable (PV) is the state that the system is actually in right now - i.e. the current velocity of the rolling ball. This value gets fed back into the controller.

  • The Controller Output (CO) is whatever value we want to set the thing that controls the system to. Itā€™s the force applied to the ball from the example above, but it can be anything really, as long as it affects the system somehow.

The original figure was a bit more complex because when youā€™re controlling actual physical systems you canā€™t just magically :sparkles: know e.g. how fast a ball is rolling or control how much force is being applied to it. You have to measure things with sensors and control valves with servos and stuff. But in the world of games programming we have the luxury of just looking things up and setting them with code :wink: You might ask why we would even want this complicated control stuff then. Canā€™t we just set the velocity that we want? Well yeah, but game engines like Roblox have a built-in physics engine that simulates how things move in the real world. By using this simulation to our advantage instead of circumventing it we can make our games feel like theyā€™re based in the real, physical world.

2. Proportional Control (aka the point, finally :exclamation:)

When talking about P (or I or D) controllers we combine the SP and PV into the error (e), or how far the system is from the desired state. e can be calculated simply as the difference of the SP and the PV:

    e = SP - PV

So if we want a velocity of 30 studs/s (SP=30) but itā€™s 10 studs/s right now (PV=10), the error at this point in time is (e=30-10=20). Or in other words, we gotta go 20 studs/s faster! At any given time, a proportional controller will simply output a CO thatā€™s proportional to the error. This just means the error multiplied by some constant which we call kP. So for a P controller,

    CO = kP * e

Simple as that!

Note that the error can be negative, like if weā€™re going too fast.

3. Programming a P controller :man_technologist:

As with all programming challenges thereā€™s loads of ways you can go about it. I want to keeps this as simple as possible programming-wise to keep things short. So Iā€™m going to implement each type of controller as a simple function. Since the P controller needs to know kP, SP and PV and outputs just a single value, Iā€™m implementing it as a function:

--The interface of the controller:
function p_controller(kP, set_point, process_value)
	--TODO: Compute CO (controller output)
	return CO
end

Since the formulas for e and CO are so simple theyā€™re EZPZ :lemon: :clamp: to program:

--The interface of the controller:
function p_controller(kP, set_point, process_value)
	local e = set_point - process_value
	local CO = kP * e
	return CO
end

ā€¦ aaaand thatā€™s it pretty much :smiley:

By the way, the SP and PV can have many different value types, not just Vector3s as in the rolling ball example. Really all they need is a way to subtract them (to do SP - PV) and to multiply them (to do kP * e). Other types like Vector2s and plain old numbers also work! You could even get weird and make your own functions for subtracting and multiplying Color3 values :red_circle::green_circle::large_blue_circle:!

4. Using the P controller :robot:

Hereā€™s how you can use the p_controller function to implement a rolling ball controller like in the example:

function update_controls()
	--Determine input roll direction
	local roll_direction = V_ZERO
	roll_direction += InputS:IsKeyDown(Enum.KeyCode.D) and V_RIGHT or V_ZERO
	roll_direction += InputS:IsKeyDown(Enum.KeyCode.A) and V_LEFT or V_ZERO
	
	--Compute desired and current roll velocity (SP and PV)
	local set_point = roll_direction * ROLL_TOP_SPEED
	local process_value = character.PrimaryPart.Velocity
	
	--Compute the force to apply
	local control_output = p_controller(ROLL_KP, set_point, process_value)
	
	--Apply the force
	character.RollForce.Force = control_output
end

RunS.RenderStepped:Connect(update_controls)

Once every frame we update the force thatā€™s applied to the ball. The ball is a Model referred to by character. V_ZERO, V_RIGHT and V_LEFT are the vectors (0, 0, 0), (1, 0, 0) and (-1, 0, 0). ROLL_KP is 300 and ROLL_TOP_SPEED is 30 in the example clips from before. If you want to see the entire, working script, check out the place file at the end of this post :slight_smile:

roll_direction is either V_LEFT, V_ZERO or V_RIGHT when the player wants to roll left, stand still or roll right, respectively. If they press both A and D they stand still. Since each component of the vector (X, Y and Z) are always either 0 or 1, multiplying roll_direction by ROLL_TOP_SPEED gives us the velocity that the system should try to reach (the SP). The PV is just the velocity that the ball currently has, and the CO is the force that gets applied to the ball.

5. Usage examples

5.1: Rolling ball

A bit more info on that: the ā€œsystemā€ is a physical body whose velocity we want to control using a VectorForce.

In many other system youā€™ll need some kind of dampening (covered in pt. 2: Derivative Control (D)). Since the ball has a bit of friction with the ground, thereā€™s some dampening built into the system. If that wasnā€™t there the velocity would overshoot (e.g. end up going too fast when speeding up) and cause oscillation back and forth and eventually cause the ball to yeet off into the distance. This can still happen if the friction doesnā€™t provide enough dampening. Try setting kP to 3000 to see what happens :skull_and_crossbones:

5.2: Making things have velocity, acceleration and force

In this example I want a GUI object to follow the mouse cursor by applying a force towards the target position. GUI objects donā€™t have velocity or acceleration because theyā€™re not really physically based. But we can make a super simple physics ā€œengineā€ from scratch! If we know the velocity of an object, we can integrate that velocity over time to figure out where itā€™s going to be at some point in the future, like this:

    next_position = current_position + velocity * dt

where dt is how far ā€œinto the futureā€ we want to calculate the position. If we do this every frame, we can get a smoothly moving simulation of something that has velocity. We can change the velocity by applying some kind of acceleration. Since our P controller outputs a force to apply to the object, we can turn that into an acceleration like so:

    F = m * a => a = F/m

where m is the mass of the body (some constant we set to whatever). We can integrate this acceleration to get change in velocity just like the position equation:

    next_velocity = current_velocity + acceleration * dt

Hereā€™s how we can use all of this math to apply a force that tries to move the GUI object towards the mouse:

function update_controls( dt )
	--Determine target and current position (SP and PV)
	local set_point = Vector2.new(mouse.X, mouse.Y)
	local process_value = cursor.AbsolutePosition

	--Compute the force to apply
	local control_output = p_controller(CURSOR_KP, set_point, process_value)

	--Apply the force
	cursor_velocity += control_output / CURSOR_MASS
	
	--Integrate velocity
	cursor.Position = UDim2.new(
		0, process_value.X + cursor_velocity.X * dt,
		0, process_value.Y + cursor_velocity.Y * dt
	)
end

RunS.RenderStepped:Connect(update_controls)

Which looks like this:

Yeah, ā€œtriesā€ is the right word, it doesnā€™t exactly do a great job of getting to the right position :roll_eyes:. This is one of the limitations of P controllers. Since thereā€™s no dampening built into this system, oscillations can just go on forever. You can see in the clip that even if the mouse is held still, the square kind of goes towards it but gets too much speed and overshoots. If this happens in both the X and Y direction it starts orbiting the mouse instead of following it!

Fun fact, this is actually how planetary orbits work :earth_africa::new_moon:. The moon is falling towards the earth at all times, but itā€™s going a bit to the side so it misses the earth and just goes around forever! Lucky for us :I

In pt. 2 on derivative control weā€™ll see how we can combine a P and D controller to actually make the square follow the cursor, so stay tuned for that!

6. Thatā€™s it folks!

Hey, you got to the end! :smiley: I hope this was helpful and informative, and not too hard to understand. If you like it so far then Iā€™m sure youā€™ll love the coming parts even more.

Iā€™d love to hear if this is something you might use, and especially if youā€™ve got feedback on this article. I want it to be as useful a resource as possible because I know that PID control has been super useful to me.

And of course feel free to ask any questions you have and Iā€™ll do my best to answer them. Anything from how it can be used in your specific case, to parts of the math or program youā€™d like clarified. If lots of people read this post then many of them will probably have the same questions, so if you ask itā€™ll help everyone :slight_smile:

If you have different ideas for other use case examples let me know and Iā€™ll add them :+1:

:heart:

7. Addendum, clarification, etc.

7.1 Sources and additional reading:

BPS.Space is an actual rocket scientist :rocket: who makes DIY rockets which are controlled withā€¦ PID! :smiley:

7.2 Credits

Text by me, figures as attributed.
OBS for recording.
NohBoard for on-screen keyboard.
Blender for video editing.


If anything needs to be added or clarified Iā€™ll probably add it hereā€¦

115 Likes

I like this article a lot, looking forward to part 2. I made a hoverboard that I think would benefit from a PID system.

2 Likes

Great tutorial! I was just making a custom character controller using vector force and was wondering if i should be using PID controllers or not. Turns out that it works spectacularly, and I thank you for this knowledge :smiley: Keep up the great work!

1 Like

I love your approach to explaining the beginning of part 2. When I tutor students on basic vector and CFrame math, I always bring up the equation ā€œdelta = final - initialā€ and your explanation aligns very closely to that, allowing for more generalized use beyond controlling forces.
High quality resource and a great read!

4 Likes

this is pretty sweet! I used PID controllers for a club irl but I didnā€™t really realize it had usage in gamedev. the explanations here are also really intuitive!

2 Likes

Really great tutorial, really taught me what these variables actually stand for and further emphasized the potential of PID.

I was inspired to make a custom AlignOrientation with only CFrames using the trick from integrating the output acceleration :+1: copied and pasted kinda/mostly from the GUI object example:

Really looking forward to part 2 in order to understand what exactly are the other two letters in the PID stand for and how to use/tune them in order to make this less wobbly :stuck_out_tongue:.

Also for anyone reading here is a PID controller I found from Sleitnicks AeroGameFramework. Unfortunately, it only works for numbers due to math.clamp there, but yeah it should work if anyone wants to go ahead and try out the rest of the ID in the PID ahead of time.

5 Likes

Thanks for the insightful and informative tutorial! Could you please provide the place file for the rolling ballā€™s working script?

Also, are you still planning on posting the other parts? If so, when can we expect part 2 on D control?

Hmm. Does a Part 2 even exist? If not, Iā€™d actually make a tutorial on the 2 other parts of the PID controller.

I would love a part 2, can we please have it.