A simple PID controller implementation in Swift. This is intended for use with SpriteKit,
SceneKit, etc, where CGFloat
is the usual type for the things you might want to PID.
Internally, calculations are done with Double. I've gotten in the habit of doing this
without really knowing if it's necessary. In my experience, PID steps are being performed
on slower tick intervals (like every 0.1 seconds) so the difference isn't going to be a
huge performance burden, but YMMV. It's pretty straightforward to replace all Double
s
with CGFloat
s and take out the explicit casts, if it does cause more harm than good.
One of the troubles I ran into was that the SpriteKit game framework doesn't make it
easy to get the current time within SKAction
completion handlers. This is where the
step(error:deltaTime:)
method comes in handy, where we can just assume that the amount
of time that has passed since the last step is approximately the duration we gave to the
SKAction
. The camera tracking example below shows how that works.
What's a PID controller?
A PID (Proportion, Integral, Derivative) controller uses a bit of calculus to smoothly
control some aspect of a physical (or simulated physical) system. It is used in
situations where you are trying to maintain a measurement (e.g. temperature, speed) at
a specific value over time in a chaotic environment, where your input to that
environment (e.g. a heating element, a gas pedal) is limited or imperfect. It runs as
a step function over time, on some interval. Each time it steps, it receives the current
error
: the difference between the desired value of the measurement, and the current
value of the measurement; it returns the next value to apply to the input (e.g. how much
energy to put into the heating element, how far to press the gas pedal, how much virtual
force to apply). Over time, it seeks to minimise the error
and maintain the value,
regardless of changing conditions.
There were a lot of borderline-meaningless words in that paragraph. I like examples. Here's an example:
A ship captain is the original PID controller. Observing ship captains was actually how
engineers figured out how to make a mechanical one. In this example, a captain has a
desired bearing and an actual bearing, both displayed on their ship compass. They want
to make the desired bearing equal to the actual bearing, and keep it there. The
difference between the desired and actual bearings is the error
in this system.
However, the ocean (much like a physics simulation) is a world of chaos. The ship may be in a storm, with unpredictable waves slamming the side and pushing it off-course. The captain can't just hold the wheel at a constant angle, or apply a constant force, to keep the ship on course. They have to constantly adapt to conditions as they are changing.
This captain is a good, experienced captain and knows how to keep the ship on course
in even the roughest of seas. They are able to see how far off course they are P
.
They see how long they've been on one side or the other of the true course I
.
They see how quickly they are approaching or leaving the true course D
. In a human
captain, these things all get boiled down into a sense of bodily coordination extended
to the ship. In mechanics and programming, we have to make them a bit more explicit.
The 3 attributes of the error that it uses (the P, I, and D) work with and against each other to stabilise the error as close to 0 as possible, without undershooting or overshooting. It leads to very smooth and intuitive motion.
A simplified explanation of each of these is:
- Proportion: How wrong are we right now? (How far is the error from
0
?) - Integral: How long have we been wrong? (How long has the error been on this side of
0
?) - Derivative: How rapidly are we becoming more right or less right? (How fast is the error changing towards/away from
0
?)
PID controllers have to be tuned, by giving it parameters that tell it how important
each of the P, I, and D are. When the PID controller returns its step value, it
multiplies each P, I, and D against its tuning parameter — named kP
, kI
, and kD
,
respectively.
Hopefully, through a brief introduction and by playing around with one, you'll get a sense of what controls what in a PID controller. I'm not going super in-depth in this section but tuning is very important and having some kind of starting place can be helpful.
My rule-of-thumb with tuning parameters is as follows:
kP
: 0.1, 1, 10, 100, 1000. Try a range of values to narrow in on a value.- Once you've tuned one thing's
kP
in your physics environment, you'll probably have a sense of what a good starting value would be. - This should generally always be significantly higher (at least 2x) either of the other parameters.
- If too much force is applied, go down. If not enough is applied, go up. This is usually the easiest parameter to tune.
- Once you've tuned one thing's
kI
: Don't set initially.- If you are never quite reaching the value you want in some cases, but your
kP
is already high enough for most circumstances, then bump this up. - If
kI
seems necessary, start at 1/2 your currentkD
value — and make sure you do have akD
value! These parameters balance each other.)
- If you are never quite reaching the value you want in some cases, but your
kD
: 1/10th of thekP
initially.- If you're overshooting (oscillating beforesettling in at the desired value) then increase it.
- If you need to "reverse-follow" a bit (e.g. a camera that responds to changes in the velocity of the node its tracking) this can be bumped way up.
This does not have clamping functionality available in some PID controllers, as I have found that
- Clamping can be done outside of the controller, and can be a bit easier to understand when it is.
- Clamping is less necessary in simulated-physics environments where you aren't as worried about overloading e.g. a motor by giving it too much power.
- It's a whole other big bag of complexity and can make debugging your PID-controlled entities even harder
There are lots of examples of clamped PID controllers online, and this one could be adapted to have clamping without too much additional work, if need be.
Say you have a camera node, and you want it to smooth-follow your player as they move
through the scene. We can use 2 PID controllers (for the x
and y
coordinates) to
both smooth out camera motion and also make the camera stay ahead of the player a bit,
if the player suddenly changes directions or has a high velocity.
// Set up camera physics
camera.physicsBody = SKPhysicsBody()
camera.physicsBody!.mass = 3.0
camera.physicsBody!.affectedByGravity = false
camera.physicsBody!.allowsRotation = false
camera.physicsBody!.linearDamping = 3
let TICK: CGFloat = 0.1 // seconds
// You would probably define this on a custom subclass of SKCameraNode
// and use `self.getPosition(within:)` instead of `camera.getPosition(within:)`
func track(_ node: SKNode,
pidX: PIDController = PIDController(kP: 4, kI: 0, kD: 2),
pidY: PIDController = PIDController(kP: 4, kI: 0, kD: 2)) {
guard let scene = scene else {
return
}
let cameraScenePosition = camera.getPosition(within: scene)
let nodeScenePosition = node.getPosition(within: scene)
let errorX = nodeScenePosition.x - cameraScenePosition.x
let errorY = nodeScenePosition.y - cameraScenePosition.y
let force = SKAction.applyForce(
CGVector(
dx: pidX.step(error: errorX, deltaTime: TICK),
dy: pidY.step(error: errorY, deltaTime: TICK)),
duration: TICK)
run(force) {
self.track(node, pidX: pidX, pidY: pidY)
}
}