Part 6 of the RayLib 2D Challenge got the 2D tile-map displaying, but scarfy could easily walk off the screen. His 2D world is larger than the screen, but there's no player tracking camera. That's today's task.

Click here to watch the video on Odysee.

The Rule of Thirds

The "rule of thirds," is a rule-of-thumb for taking aesthetically pleasing photos. In a nutshell, you place the key subject in your photo a third in from one of the sides. I'm going to use this rule. Scarfy should be a third in from whichever side is behind him. That way you can see what's behind him, but two thirds of the screen shows what's in front of him (the area you most likely need to see).

The rule of thirds will only be used horizontally. Vertically, scarfy will be placed in the middle.

How do we know which way scarfy is facing? The simplest way is simply to check if scarfy's horizontal velocity is left or right, which looks as follows in code:

if(target->velocity.x > 0) {
	int screenWidth = GetScreenWidth();
	targetOffset = -raylib::Vector2(screenWidth / 6.0f, 0.0f);
} else if(target->velocity.x < 0) {
	int screenWidth = GetScreenWidth();
	targetOffset = raylib::Vector2(screenWidth / 6.0f, 0.0f);
}

This sets the targetOffset to plus or minus 1/6th of the screen width, placing the target offset on the left or right third of the screen.

Things that Don't Work

Writing code often involves experimentation. My original player tracking code adjusted the camera's velocity by a fraction of the difference between where the camera is pointed, and where the player is:

auto error = target->position - (this->position + targetOffset);
velocity += error * accelerationFactor;

This caused nauseous vomit-inducing oscillations because the tracking code continually overshot the target and then swung back.

Watch the video to see it in action. It's spectacularly bad!

Clearly, we need something more sophisticated...

Using PID Controller

Proportional-Integral-Derivative (PID) controllers are used extensively to control machinery. They're relatively simple to implement, and are very effective at steering systems (plant/process in control theory lingo) to a desired "state." Sounds perfect for our tracking camera. The "desired state," is pointing at scarfy while obeying the rule of thirds. 

We'll drop the derivative term, and go with a simpler PI controller. Here's the basic code:

auto error = target->position - (this->position + targetOffset);
if(error.Length() < targetRadius) {
	integral = raylib::Vector2::Zero();
}
integral += error;
velocity = error * propFactor + integral * integralFactor;

position += velocity;

As you can see, it measures the error between where the camera should be looking and where it is. The integral variable accumulates the error over time (i.e., calculates the integral), and the velocity is calculated from the error and integral with two multiplication factors. These factors need to be "tuned" to suit the system, which is done by experimentation.

Eagle-eyed readers will notice the integral reset code, which is triggered when the camera gets close to where it should be (look for targetRadius). PID controllers tend to overshoot and oscillate, or take ages to reach the target. Both options are unacceptable, so we reset the integral once the camera gets close to where it should be.

PID Controller V2

The code above still didn't give the smooth tracking I wanted. I could spend ages tuning PID factors. Or, I could get clever. The player's velocity is readily available, so why not use it as another control input? In code:

velocity = error * propFactor + integral * integralFactor +  targetVelocity;

Adding the targetVelocity allows the camera to almost instantly match the player's speed. The original PID controller adjusts for any discrepancies. With this change done, the camera is working nicely.

The Final Code

I added one more feature after recording the video: clamping the camera to the scene boundaries. This is more complicated than it sounds, because it interacts with the PI controller. Care must be taken, or the controller will go beserk due to being unable to reach the target point.

Here's what the final code looks like:

// Using the photography "rule of thirds" horizontally,
// so calculate the offset based on the direction that the
// target is moving
if(target->velocity.x > 0) {
	int screenWidth = GetScreenWidth();
	targetOffset = -raylib::Vector2(screenWidth / 6.0f, 0.0f);
} else if(target->velocity.x < 0) {
	int screenWidth = GetScreenWidth();
	targetOffset = raylib::Vector2(screenWidth / 6.0f, 0.0f);
}

// This is a basic PI controller. See:
// https://en.wikipedia.org/wiki/PID_controller
auto error = target->position - (this->position + targetOffset);
auto targetVelocity = target->velocity;

if(sceneSize.Length() > 0) {
	// Clamp the camera to the scene size
	auto cameraOffset = GetOffset();
	if(position.x - cameraOffset.x <= 0) { 
		position.x = cameraOffset.x; 
		if(error.x < 0) { 
			error.x = 0.0f; 
			integral.x = 0.0f;
			targetVelocity.x = 0.0f;
		}
	}
	if(position.y - cameraOffset.y <= 0) { 
		position.y = cameraOffset.y; 
		if(error.y < 0) { 
			error.y = 0.0f; 
			integral.y = 0.0f;
			targetVelocity.y = 0.0f;
		}
	}
	if(position.x + cameraOffset.x >= sceneSize.x) { 
		position.x = sceneSize.x - cameraOffset.x; 
		if(error.x > 0) { 
			error.x = 0.0f; 
			integral.x = 0.0f;
			targetVelocity.x = 0.0f;
		}
	}
	if(position.y + cameraOffset.y >= sceneSize.y) { 
		position.y = sceneSize.y - cameraOffset.y;
		if(error.y > 0) { 
			error.y = 0.0f; 
			integral.y = 0.0f;
			targetVelocity.y = 0.0f;
		}
	}
}

if(error.Length() < targetRadius) {
	integral = raylib::Vector2::Zero();
}
integral += error;
velocity = error * propFactor + integral * integralFactor
	+ targetVelocity;

this->position += this->velocity;

What's Next?

There's an obvious problem still to be solved (watch the video): scarfy is walking through the terrain instead of on it. That's because there's no collision detection yet. We'll work on that next time...

Click here for part 8.

Source Code

Click here to download the source code.