TL;DR: Section 2 for math, section 0 for teaser, sections 3, 4 for code, section 5 for examples
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
0. Teaser
First up is a teaser of why P control is awesome!
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
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 . 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 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 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 )
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
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 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
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 !
4. Using the P controller
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
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
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 . 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 . 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! 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
If you have different ideas for other use case examples let me know and I’ll add them
7. Addendum, clarification, etc.
7.1 Sources and additional reading:
- Video from MatLab: Understanding PID Control, Part 1: What Is PID Control? - YouTube (check it out, it’s awesome!)
- Video series from Brian Douglas: PID Control - A brief introduction - YouTube (also very awesome! And how I first learned about PID)
- PID controller - Wikipedia
- Control loop - Wikipedia
BPS.Space is an actual rocket scientist who makes DIY rockets which are controlled with… PID!
- Modeling a Thrust Vectored Rocket In Simulink - YouTube
- Reaction Control System(RCS) Development - YouTube
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…