PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Android Games

DDDDD
Number Blocks
Match 3 Warriors

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
2D run and gun
Roguelike
Medals (Achievements)
2D turn-based strategy game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 turn-based strategy tutorial
Thu, 14th April 2022

Water Closet ported to PlayStation Vita
Tue, 4th January 2022

The Legend of Edgar 1.35
Sat, 1st January 2022

Achievements tutorial
Thu, 2nd December 2021

SDL2 Rogue tutorial
Thu, 30th September 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (7)
games (39)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (10)
water-closet (4)

Books


H1NZ

Arriving on the back of a meteorite, an alien pathogen has spread rapidly around the world, infecting all living humans and animals, and killing off all insect life. Only a handful are immune, and these survivors cling desperately to life, searching for food, fresh water, and a means of escape, find rescue, and discover a way to rebuild.

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 22: Smooth camera

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

The current camera we have in place works pretty well, but one problem is that when it comes to the AI turn (and with the player unit selection), the camera jumps straight to the unit. This can be disorientating, and doesn't look great. In this part, we're going to change the camera, so it moves smoothly to its target.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS22 to run the code. You will see a window open like the one above, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Notice how the camera smoothly moves from one unit to another, as needed. This helps us to see the battlefield in context, and doesn't lead to any sudden lurches. Once you're finished, close the window to exit.

Inspecting the code

As expected, the major focus of the updates in this part is to the camera code, with other parts of the code requiring just simple one-line changes.

Let's being with structs.h:


typedef struct {

	// snipped

	struct {
		double x;
		double y;
		int tx;
		int ty;
		int scrolling;
		double scrollDelay;
	} camera;
	struct {
		int numMages, totalMages;
		int numGhosts, totalGhosts;
		int rounds;
		int bulletsFired, bulletsHit;
		int numMoves;
		int numPancakes, totalPancakes;
		int numAmmo, totalAmmo;
		double timePlayed;
	} stats;
} Stage;

Our Stage struct is all we need to update here. We've added to the existing anonymous `camera` struct inside of it. We're now introduced four new fields: `tx` and `ty` are the target x and target y location that the camera wants to move to. This will be, for example, the x and y screen coordinates of a unit or map tile. `scrolling` is a flag to say whether the camera is currently in motion. When it is, it will block all interaction with the camera, sort of like Stage's `animating` flag. Finally, scrollDelay is a variable to act as a timer before the camera begins to move. In the AI's turn, we'll want to pause for a moment before going to the next enemy unit, to, again, do away with the whiplash effect of the camera suddenly shifting.

No, let's head over to camera.c, where we've made the bulk of the changes. Starting with doCamera, a new function:


void doCamera(void)
{
	double diffX, diffY, dist;

	stage.camera.scrollDelay = MAX(stage.camera.scrollDelay - app.deltaTime, 0);

	if (stage.camera.scrolling && stage.camera.scrollDelay == 0)
	{
		diffX = fabs(stage.camera.tx - stage.camera.x);
		diffY = fabs(stage.camera.ty - stage.camera.y);

		dist = MAX(diffX, diffY);

		diffX /= 10;
		diffY /= 10;

		diffX = MAX(0, MIN(MAX_SCROLL_SPEED, diffX));
		diffY = MAX(0, MIN(MAX_SCROLL_SPEED, diffY));

		if (stage.camera.x > stage.camera.tx)
		{
			stage.camera.x -= diffX;
		}

		if (stage.camera.x < stage.camera.tx)
		{
			stage.camera.x += diffX;
		}

		if (stage.camera.y > stage.camera.ty)
		{
			stage.camera.y -= diffY;
		}

		if (stage.camera.y < stage.camera.ty)
		{
			stage.camera.y += diffY;
		}

		stage.camera.scrolling = dist > MAX_SCROLL_SPEED;
	}
}

The doCamera function is responsible for actually driving the motion of the camera. Our camera will move so long as the scrolling delay value is 0 and also if it is not yet near its target location.

The first thing we're doing is decreasing the value of camera's scrollDelay, locking it at 0. Next, we're testing whether camera's `scrolling` flag is set and also if scrollDelay is 0. If both of these are true, it means that our camera is free to move. We start by working out the difference between the camera's `x` and `y` and its `tx` (target x) and `ty` (target y). These, we're assigning to variables called diffX and diffY. With these known, we assign a variable called `dist` the greater of the two values. `dist` will now hold the maximum distance our target location is from our present location.

Next, we're dividing both diffX and diffY by 10, and then limiting these values to between 0 and MAX_SCROLL_SPEED (defined in camera.h as 12). What this will do is give us values that we use for the speed that the camera wants to move at. The further away the camera is, the greater the values of diffX and diffY. However, we don't want these values to be too large, as it will mean our camera will scroll too fast (or, as we had previously, jump instantly to the target location).

Now that we have our horizontal and vertical speeds (diffX and diffY), we test where the camera's `x` and `y` values are compared to its `tx` and `ty`. We'll then adjust the camera's `x` and `y` by diffX and diffY (either adding or subtracting) to make it move. So, in effect, we're making the camera's `x` and `y` increase or decrease in order to reach its target location.

Finally, we update the camera's `scrolling` flag. We want our camera to stop moving once it is within range of its `tx` and `ty` (we don't need it to be exact). If the value of dist is greater than MAX_SCROLL_SPEED, we're still not yet within reach of our target location, and so we'll set it to 1. Otherwise, we'll set it to 0. We don't want to try and match the location exactly here, as that will often not happen, and our camera will get stuck.

That's our main camera logic done! That wasn't too bad, eh? What follows for the rest of this part will also very straight forward. Continuing with the updates in camera.c, we've tweaked the centreCameraOn function:


void centreCameraOn(Entity *e, int scrollTo)
{
	int x, y;

	x = MAP_TO_SCREEN(e->x) - SCREEN_WIDTH / 2;
	y = MAP_TO_SCREEN(e->y) - SCREEN_HEIGHT / 2;

	if (scrollTo)
	{
		stage.camera.tx = x;
		stage.camera.ty = y;
	}
	else
	{
		stage.camera.x = x;
		stage.camera.y = y;
	}

	stage.camera.scrolling = scrollTo;

	clipCamera();
}

It now takes an argument called scrollTo. This is a flag to say whether we want our camera to smoothly move to the target entity or immediately jump to them. As before, we're working out the screen position of the entity (as `x` and `y`), but are then testing whether the scrollTo flag is set. If so, we're setting the camera's `tx` and `ty` to the values of `x` and `y`. Otherwise, it's the camera's `x` and `y` that are updated as before. We're then setting camera's `scrolling` flag as the value of scrollTo, to make it move if required.

As we saw in doCamera, if we're scrolling, we'll move to the `tx` and `ty` values. This function now sets that up.

To demonstrate this, let's look at the change to ensureOnScreen:


void ensureOnScreen(Entity *e)
{
	int x, y;

	x = e->x - (stage.camera.x / MAP_TILE_SIZE);
	y = e->y - (stage.camera.y / MAP_TILE_SIZE);

	if (!(x >= ENT_ON_SCREEN_BOUNDS && y >= ENT_ON_SCREEN_BOUNDS && x <= MAP_RENDER_WIDTH - ENT_ON_SCREEN_BOUNDS && y < MAP_RENDER_HEIGHT - ENT_ON_SCREEN_BOUNDS))
	{
		centreCameraOn(e, 1);
	}
}

To conform to the new function signature, we're passing over the extra parameter. In this case, we want the camera to smoothly scroll.

Now, let's head over to stage.c, where we've updated `logic` with a very important change:


static void logic(void)
{
	int wasAnimating;

	doCamera();

	if (!stage.camera.scrolling)
	{
		wasAnimating = stage.animating;

		if (stage.stats.numMages > 0 && stage.stats.numGhosts > 0)
		{
			stage.stats.timePlayed += app.deltaTime;
		}
		else
		{
			endTimer = MAX(endTimer - app.deltaTime, 0);

			stage.animating = 1;
		}

		// snipped
	}
}

Before all else, we're calling doCamera. We're then testing to see whether our camera's `scrolling` flag is set. If not, our game's logic is free to run as usual. Otherwise, our game will essentially pause while the camera is in motion.

That's all the major work to get our smooth camera flowing done. Now we just need to tweaks several other files to make use of the new function. These will be, as expected, all one or two line changes and additions.

First, let's head to ai.c, where we've updated nextUnit:


static void nextUnit(void)
{
	int found;

	// snipped

	if (stage.currentEntity != NULL)
	{
		updateUnitRanges();

		ensureOnScreen(stage.currentEntity);
	}

	stage.camera.scrollDelay = FPS / 4;
}

At the end of nextUnit, we're setting the camera's scrollDelay value to a quarter of a second. This is so that when the AI unit has finish taking its turn, the camera doesn't instantly move to the next unit or back to the player. This is the sole reason the scrollDelay value exists, so that the player isn't disorientated by the sudden shift.

Next, over to hud.c:


static void doCommandButtons(void)
{
	int i;

	hoverCommandButton = NULL;

	for (i = 0 ; i < CI_MAX ; i++)
	{
		if (collision(app.mouse.x, app.mouse.y, 1, 1, commandButtons[i].x, commandButtons[i].y, commandButtons[i].w, commandButtons[i].h))
		{
			hoverCommandButton = &commandButtons[i];

			if (app.mouse.buttons[SDL_BUTTON_LEFT])
			{
				app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

				switch (i)
				{
					case CI_PREV_UNIT:
						cyclePlayerUnits(-1);
						break;

					case CI_NEXT_UNIT:
						cyclePlayerUnits(1);
						break;

					case CI_CENTER:
						centreCameraOn(stage.currentEntity, 1);
						break;

					// snipped
				}
			}
		}
	}

	// snipped
}

We've updated the call to centreCameraOn, to conform to the new function signature. Here, we're passing over 1, to make the camera smoothly scroll to the unit.

Heading over to player.c, we've done likewise with addPlayerUnits:


static void addPlayerUnits(void)
{
	Entity *e;
	int i, mx, my, x, y, ok;
	char *names[] = {"Andy", "Danny", "Izzy"};

	// snipped

	stage.currentEntity = units[0];

	updateUnitRanges();

	centreCameraOn(stage.currentEntity, 0);
}

Here, however, we're telling the camera to jump straight to the player unit's location. This is the only place in the code we're doing this. We don't want the camera to fly across the map when the game starts, as it looks a bit odd.

And there you go! A camera that smoothly scrolls across the battlefield as we need it to. I'm sure you'll agree that this is far more preferrable than the jumping camera we had before, and it wasn't all that difficult to setup or incorporate.

One last major thing we want to do now is look into the map generation. Perhaps you've noticed that from time to time our game appears to freeze when starting up, as it creates the map. This freezing can become more pronounced if we simply increase the size of the map. Doubling the values, for example, will lead to the game getting stuck for several seconds. That's not very good. So, in our penultimate part, we'll look into solving this by using some threads, to make the experience a little more pleasant for the player.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase:

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Directly

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal, and then download the tutorials directly from the main tutorials page.

SDL2_Tutorials.tar.gz 56.76MB 23rd April 2022

Click here to see the list of files in the archive

Mobile site