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
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

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

Books


The Battle for the Solar System (Complete)

The Pandoran war machine ravaged the galaxy, driving the human race to the brink of destruction. Seven men and women stood in its way. This is their story.

Click here to learn more and read an extract!

« Back to tutorial listing

— A simple turn-based strategy game —
Part 13: Adding the camera

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

Introduction

It's time to increase the size of the map, and, in turn, add in the camera. We're going to be adding in a smooth scrolling map, as found in SDL2 Gunner, rather than the "jump" scrolling we have in SDL2 Rogue and SDL2 Adventure.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS13 to run the code. You will see a window open like the one above, showing three wizards in a large room, surrounded by walls, as well as a number of ghosts. Play the game as normal. To scroll the map, use the WASD control scheme. Notice how the map attempt to keep subjects on screen during movements; when the player select mages, either with the mouse wheel or a click; and, when attacks occur, the target is centered. Once you're finished, close the window to exit.

Inspecting the code

Adding in our camera and increasing the size of our map is a simple thing, indeed. It just means having to update a lot of files!

Let's start with defs.h:


#define MAP_WIDTH                 60
#define MAP_HEIGHT                30

#define MAP_RENDER_WIDTH          ((SCREEN_WIDTH / MAP_TILE_SIZE) + 1)
#define MAP_RENDER_HEIGHT         ((SCREEN_HEIGHT / MAP_TILE_SIZE) + 1)

We've increased the values of MAP_WIDTH and MAP_HEIGHT, and also added in MAP_RENDER_WIDTH and MAP_RENDER_HEIGHT, to deal with our map rendering as we scroll it around.

Moving next to structs.h, we've updated Stage:


typedef struct {
	unsigned int entityId;
	MapTile map[MAP_WIDTH][MAP_HEIGHT];
	Entity entityHead, *entityTail;
	Entity deadEntityHead, *deadEntityTail;
	Entity *currentEntity, *targetEntity;
	Effect effectHead, *effectTail;
	DamageText damageText;
	Node routeHead;
	int turn;
	int animating;
	int showRange;
	Bullet bullet;
	struct {
		int x;
		int y;
	} selectedTile;
	struct {
		double x;
		double y;
	} camera;
} Stage;

It now contains a struct called `camera`, that will hold our camera data. We're planning on updating this struct in a future part.

Now, let's look at camera.c. This is a new compilation unit, that will hold all the code for our camera. We'll start with clipCamera:


void clipCamera(void)
{
	stage.camera.x = MIN(MAX(stage.camera.x, -W_PADDING), (MAP_WIDTH * MAP_TILE_SIZE) - (SCREEN_WIDTH  - W_PADDING));
	stage.camera.y = MIN(MAX(stage.camera.y, -H_PADDING), (MAP_HEIGHT * MAP_TILE_SIZE) - (SCREEN_HEIGHT - H_PADDING));
}

Much like the updateCamera function in SDL2 Gunner, this function limits how far the camera is allowed to scroll, making use of the MAX and MIN macros, applied to both the camera's `x` and `y`. Note how we're not clipping to camera to 0,0 or the width and height of the map. We're allowing the minimum and maximum to be adjusted by W_PADDING and H_PADDING. This basically gives us some overscan, so we can scroll beyond the limits of the map. This can make things more comfortable for the player, as they can move the corners of the map closer to the centre of the screen.

Next up is centreCameraOn:


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

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

	stage.camera.x = x;
	stage.camera.y = y;

	clipCamera();
}

This function is used to centre the camera on an entity (`e`). We convert the entity's `x` and `y` values to screen coordinates, then subtract half the screen width and height, and assign the result to the camera's `x` and `y`. Finally, we call clipCamera, to ensure we don't stray outside our allowed map overscan.

The last function is 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);
	}
}

As it name suggests, this function will ensure that an entity (`e`) is visible on screen. To achieve this, we want to make sure that our entity is contained within a certain area of the screen. We take the entity's `x` and `y` values, and subtract the camera's `x` and `y`, divided by MAP_TILE_SIZE. This results in `x` and `y` having values that can be evaluated as though they are indexes of the map. We then test the values of `x` and `y`. If `x` or `y` are less than ENT_ON_SCREEN_BOUNDS, we will call centerCameraOn, passing over `e`. Equally, if `x` or `y` are outside of MAP_RENDER_WIDTH less ENT_ON_SCREEN_BOUNDS or MAP_RENDER_HEIGHT less ENT_ON_SCREEN_BOUNDS, respectively, we'll also call centerCameraOn. Ultimately, this will mean that calling this function will keep the subject entity in the middle of the screen. We'll see this in a use in a little while.

That's camera.c done, for now. Next, let's head over to player.c, where we've made a number of little updates. Starting with doControls:


static void doControls(void)
{
	if (app.keyboard[SDL_SCANCODE_SPACE])
	{
		app.keyboard[SDL_SCANCODE_SPACE] = 0;

		endTurn();
	}

	if (app.keyboard[SDL_SCANCODE_W])
	{
		stage.camera.y -= CAMERA_SPEED * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_S])
	{
		stage.camera.y += CAMERA_SPEED * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_A])
	{
		stage.camera.x -= CAMERA_SPEED * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_D])
	{
		stage.camera.x += CAMERA_SPEED * app.deltaTime;
	}
}

We're now testing for our WASD control screen, to move the camera around. Based on the keys used, we'll either increase or decrease the camera's `x` and `y`, by CAMERA_SPEED. This will cause the camera scroll smoothly around.

Next, we've updated cyclePlayerUnits:


static void cyclePlayerUnits(int dir)
{
	int i;

	for (i = 0 ; i < NUM_PLAYER_UNITS ; i++)
	{
		if (units[i] == stage.currentEntity)
		{
			do
			{
				i += dir;

				if (i < 0)
				{
					i = NUM_PLAYER_UNITS - 1;
				}
				else if (i >= NUM_PLAYER_UNITS)
				{
					i = 0;
				}

				stage.currentEntity = units[i];
			}
			while (stage.currentEntity->dead);

			ensureOnScreen(stage.currentEntity);

			updateUnitRanges();

			return;
		}
	}
}

After completing our while-loop, we've added in a call to ensureOnScreen, to make sure that the unit we've newly changed to is on screen. This means that as we scroll through our available mages, the camera will jump to them, rather than us having to find out where they are (but only if they aren't already visible on screen).

We've done the same thing with doSelectUnit:


static void doSelectUnit(void)
{
	Entity *e;
	Unit *u;

	if (app.mouse.buttons[SDL_BUTTON_LEFT])
	{
		u = (Unit*) stage.currentEntity->data;

		e = getEntityAt(stage.selectedTile.x, stage.selectedTile.y);

		if (e != NULL && e->solid)
		{
			app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

			if (e->side == SIDE_PLAYER)
			{
				if (e == stage.currentEntity)
				{
					if (++stage.showRange >= SHOW_RANGE_MAX)
					{
						stage.showRange = SHOW_RANGE_NONE;
					}
				}
				else
				{
					stage.currentEntity = e;

					updateUnitRanges();
				}

				ensureOnScreen(e);
			}
			else if (e->side == SIDE_AI)
			{
				if (stage.targetEntity != e)
				{
					stage.targetEntity = e;
				}
				else
				{
					attackTarget(u);
				}
			}
		}
	}

	// snipped
}

Now, after selecting a mage (or cycling through the unit's actions), we're calling ensureOnScreen, in case the mage is at the limits of the screen. Again, we're doing this to make things a little more comfortable for the player.

Now, let's have a quick look at how the addition of the camera is affecting our rendering of entities and the map. Once again, we've actually covered this in past tutorials, so we won't linger or go into depth here.

Looking first at units.c, we've updated `draw`:


static void draw(Entity *self)
{
	int x, y;
	Unit *u;

	u = (Unit*) self->data;

	x = MAP_TO_SCREEN(self->x) - stage.camera.x;
	y = MAP_TO_SCREEN(self->y) - stage.camera.y;

	if (u->shudder > 0)
	{
		x += sin(shudderAmount) * u->shudder;
	}

	blitAtlasImage(self->texture, x, y, 1, self->facing == FACING_RIGHT ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL);
}

We're now subtracting the camera's `x` and `y` from the screen coordinates of the unit, before drawing. This will ensure that the unit is drawn in the correct place.

As another example, we've updated damageText.c:


void drawDamageText(void)
{
	DamageText *d;
	int x, y;

	d = &stage.damageText;

	if (d->life > 0)
	{
		x = d->x - stage.camera.x;
		y = d->y - stage.camera.y;

		app.fontScale = 0.8;

		drawText(d->text, x + 2, y + 2, 0, 0, 0, TEXT_ALIGN_CENTER, 0);

		drawText(d->text, x, y, 255, 255, 255, TEXT_ALIGN_CENTER, 0);

		app.fontScale = 1.0;
	}
}

Yet again, we're subtracting the camera's `x` and `y` from the damageText's `x` and `y`, to make sure we display it in the correct location on screen.

We've done the same for all entities and effects, int their respective functions, but we won't detail them all.

Briefly, let's look at map.c and the drawMap function:


void drawMap(void)
{
	int x, y, x1, x2, y1, y2, mx, my, n;

	x1 = ((int) stage.camera.x % MAP_TILE_SIZE) * -1;
	x2 = x1 + MAP_RENDER_WIDTH * MAP_TILE_SIZE + (x1 == 0 ? 0 : MAP_TILE_SIZE);

	y1 = ((int) stage.camera.y % MAP_TILE_SIZE) * -1;
	y2 = y1 + MAP_RENDER_HEIGHT * MAP_TILE_SIZE + (y1 == 0 ? 0 : MAP_TILE_SIZE);

	mx = (int) stage.camera.x / MAP_TILE_SIZE;
	my = (int) stage.camera.y / MAP_TILE_SIZE;

	for (y = y1 ; y < y2 ; y += MAP_TILE_SIZE)
	{
		for (x = x1 ; x < x2 ; x += MAP_TILE_SIZE)
		{
			if (isInsideMap(mx, my))
			{
				n = stage.map[mx][my].tile;

				blitAtlasImage(tiles[n], x, y, 0, SDL_FLIP_NONE);

				if (!stage.animating && stage.showRange != SHOW_RANGE_NONE)
				{
					if (stage.showRange == SHOW_RANGE_MOVE && stage.map[mx][my].inMoveRange)
					{
						blitAtlasImage(moveTile, x, y, 0, SDL_FLIP_NONE);
					}
					else if (stage.showRange == SHOW_RANGE_ATTACK && stage.map[mx][my].inAttackRange)
					{
						blitAtlasImage(attackTile, x, y, 0, SDL_FLIP_NONE);
					}
					else
					{
						blitAtlasImage(darkTile, x, y, 0, SDL_FLIP_NONE);
					}
				}
			}

			mx++;
		}

		mx = stage.camera.x / MAP_TILE_SIZE;

		my++;
	}
}

We've updated this function to now take into account the position of the camera. This rendering technique was covered in SDL2 Gunner, so we'll say nothing more on it here, only that we've implemented it. Note that our tile drawing, with the ranges, remains the same.

Unlike SDL2 Gunner, however, we're not using a quadtree in this game, as there is no need. To that end, we want to ensure that we only draw the entities that are actually on screen right now (something the quadtre was helping us with in SDL2 Gunner). Let's take a look at the updates to entities.c, to see how this is done.

Start with drawEntities:


void drawEntities(void)
{
	Entity *e, *drawList[MAX_DRAW_ENTS];
	int i, n, x, y, size;

	n = 0;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (n < MAX_DRAW_ENTS && isOnScreen(e))
		{
			drawList[n++] = e;
		}
	}

	qsort(drawList, n, sizeof(Entity*), drawComparator);

	for (i = 0 ; i < n ; i++)
	{
		e = drawList[i];

		e->draw(e);
	}

	// snipped
}

Before, we were simply rendering all the entities. Since our map was small and was contained within the one screen, we didn't need to worry about drawing entities that were outside of our view. Now, we're going to take this into account. The idea is to test all entities in the game, to see if they are visible, and add them to a draw list, beforing rendering them.

We start by setting a variable called `n` to 0. This will be a count of all the entities that are on screen at this time. Next, we loop through all our entities, and pass them into a function called isOnScreen. We also test that `n` is less than MAX_DRAW_ENTS, to make sure we don't overwrite our array. Should this test pass, we add the entity to drawList.

Next, we're using qsort to sort the list of entities to draw. This step will help to draw our entities in the correct order. You may have noticed in previous parts that when a mage or ghost stops over an item that the item is drawn on top of them. This is because we're rendering the entities in the order of the list, which isn't desirable. Since items are the last things to be added to our entity list, they are drawn last and obscure our entities.

Finally, with drawList populated, we loop through all the elements and draw the entity at the index.

The isOnScreen function follows:


static int isOnScreen(Entity *e)
{
	int x, y;

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

	return (x >= -1 && y >= -1 && x < MAP_RENDER_WIDTH + 1 && y < MAP_RENDER_HEIGHT + 1);
}

This is quite a lot like the ensureOnScreen function in camera.c. However, ror the purposes of rendering, we're allowing our entities to be slightly outside the bounds of the screen, so they don't suddenly pop into existence as we scroll.

Finally, the drawComparator:


static int drawComparator(const void *a, const void *b)
{
	Entity *e1 = *((Entity**)a);
	Entity *e2 = *((Entity**)b);

	return e1->type - e2->type;
}

This is the function we're feeding into qsort. We'll sort our entities by their type, with those with a lower type value being pushed to the bottom. According to our ET enum order, ET_ITEM will be drawn first, with the other things drawn afterwards. This means that items cannot obscure our units.

We're almost done! We're going to take a quite look at where else the ensureOnScreen function is used, and then we'll be wrapping this part up.

Turning first to bullets.c and the fireBullet function:


void fireBullet(void)
{
	// snipped

	u->ap--;

	u->weapon.ammo--;

	ensureOnScreen(stage.targetEntity);
}


At the end of the function, we're calling ensureOnScreen. Note that we're passing over Stage's targetEntity. The reason for this is because we want the target of the attack to be visible, rather than the attacker. It's no good if the attacker can be seen clearly, but victim is invisible due to being offscreen. If the player is being attacked, they'll want to know which of their units is the target.

We've added ensureOnScreen to the move function in units.c:


static void move(void)
{
	Node *n;

	moveTimer -= app.deltaTime;

	if (moveTimer <= 0)
	{
		// snipped

		if (stage.routeHead.next == NULL)
		{
			((Unit*) stage.currentEntity->data)->ap--;

			resetAStar();

			collectItems();
		}

		ensureOnScreen(stage.currentEntity);

		moveTimer = 5;
	}
}

As our unit moves, we're calling ensureOnScreen, to keep them on screen. This prevents the unit from walking off screen and their movements becoming lost to the player.

We've also added ensureOnScreen to resetUnits:


void resetUnits(void)
{
	Entity *e;
	Unit *u;

	stage.currentEntity = stage.targetEntity = NULL;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if ((stage.turn == TURN_PLAYER && e->type == ET_MAGE) || (stage.turn == TURN_AI && e->type == ET_GHOST))
		{
			u = (Unit*) e->data;

			u->ap = u->maxAP;

			if (stage.currentEntity == NULL)
			{
				stage.currentEntity = e;

				updateUnitRanges();

				ensureOnScreen(e);
			}
		}
	}
}

Doing this makes sure that when the player's or AI's turn begins that the camera focuses on the unit that has now become activated.

Finally, let's head to stage.c, and update the `logic` function:


static void logic(void)
{
	int wasAnimating;

	wasAnimating = stage.animating;

	if (!stage.animating)
	{
		if (stage.turn == TURN_PLAYER)
		{
			doHud();

			doPlayer();
		}
		else
		{
			doAI();
		}
	}

	doEntities();

	doUnits();

	doBullet();

	doEffects();

	doDamageText();

	clipCamera();

	stage.animating = stage.routeHead.next != NULL || stage.bullet.life > 0 || stage.effectHead.next != NULL || stage.damageText.life > 0;

	app.mouse.visible = !stage.animating && stage.turn == TURN_PLAYER;

	if (wasAnimating && !stage.animating)
	{
		updateUnitRanges();
	}
}

After almost everything is done, we're calling clipCamera. We're doing this here to centralize the logic, so that we don't need to keep calling it every time the camera is moved about. Doing this near the end of the function means we're close to the rendering phase, so everything will draw as expected.

And that's our camera added in. This is infact not the end of our camera work, and in a future part we're going to improve it. For now, we have something that is functional and easy to work with.

What would be nice now is if we had a better HUD. We should be able to display messages and supply some basic UI elements to control the game. In the next part, we're going to look into adding these in.

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:

Mobile site