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

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
2D quest game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Quest game tutorial
Wed, 7th May 2025

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

All Updates »

Tags

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

Books


The Honour of the Knights (Second Edition) (Battle for the Solar System, #1)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and the Mitikas Empire's civil war than the Helios Confederation is willing to let on. Somewhere out there the Pandoran army is gathering, preparing to bring ruin to all the galaxy...

Click here to learn more and read an extract!

« Back to tutorial listing

— Simple 2D quest game —
Part 4: Adding the player

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

Introduction

Now that we have our world and islands generated, it's about time we added the ability to explore the world. In this part, we'll be adding in our robot adventurer, who will be able to walk around on land, to see what it has to offer. As our robot moves from square to square in a single jump, this will be very easy to do (we've already seen it done in SDL2 Adventure and SDL2 Rogue). Our robot won't be able to move into sea, forest, or mountain tiles.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest04 to run the code. As before, you can move around with the WASD control scheme, only this time you will be controlling the robot. The camera will track your movement as you explore. Pressing Tab will open the Quest Log, so you can get an idea of where you are (you will be placed on the largest island). However, your exact location is currently not displayed. When you're finished, close the window to exit.

Inspecting the code

If you've been working through these tutorials (or have at least investigated SDL2 Adventure / SDL2 Rogue), what follows will be quite familiar. As we've touched on things like entity creation in the past, we'll not be looking at anything to do with entity management and processing, and instead just the parts that are important to our quest game. So, let's make a start with the code updates.

First up, we have the Entity struct:


struct Entity
{
	unsigned int id;
	int          type;
	char         name[MAX_NAME_LENGTH];
	int          x;
	int          y;

	// snipped

	Entity *next;
};

There's not a lot to say about this, as we've seen it before. One thing to note is the inclusion of an `id`. Each entity that is created will be given a unique, incrementing id. This is to enable them to be identified wherever they are in the world. The entity factory is responsible for setting and incrementing this id, whenever we create a new Entity (via initEntity).

Next up, we've updated the Map struct:


typedef struct
{
	int       width;
	int       height;
	int     **data;
	Entity    entityHead, *entityTail;
	Entity   *player;
	SDL_Point camera;
} Map;

We've added in support for our entity linked list (entityHead and entityTail). Again, be aware that the overworld and towns will all have separate maps, and therefore will have their own collections of entities. This is partly why we need the unique id field in the Entity struct, so that we can find an entity by its id, anywhere; we don't want two entities to exist in our game both having an id of 0, for example.

With that done, we can look at how our player is added to the world. If we head over to overworldGen.c, we've can see a few tweaks have been made. First up, we've updated generateOverworldWorker:


static int generateOverworldWorker(void *p)
{
	double **elevation, **moisture;
	long     then;

	// snipped

	addAdventurer();

	// snipped

	return 0;
}

We're making a call to a new function called addAdventurer:


static void addAdventurer(void)
{
	Entity *e;
	Island *is, *startingIsland;
	int     x, y, ok, w, h;

	startingIsland = NULL;

	for (is = game.overworld.islandHead.next; is != NULL; is = is->next)
	{
		if (startingIsland == NULL || is->size > startingIsland->size)
		{
			startingIsland = is;
		}
	}

	w = (startingIsland->bounds.x2 - startingIsland->bounds.x1);
	h = (startingIsland->bounds.y2 - startingIsland->bounds.y1);

	e = initEntity("Adventurer");

	do
	{
		x = startingIsland->bounds.x1 + rand() % w;
		y = startingIsland->bounds.y1 + rand() % h;

		ok = getEntityAt(x, y) == NULL && game.map->data[x][y] >= MT_SHALLOWS && game.map->data[x][y] <= MT_LAND;
	} while (!ok);

	e->x = x;
	e->y = y;

	game.map->player = e;
}

This function is responsible for adding our robot adventurer to the world. When we add in our robot, we want to place them on the largest island in the world. We do this simply by looping through all the islands, finding the one with the greatest size, and setting that as our starting Island (as startingIsland). With the island found, we then create the Adventurer entity, and attempt to randomly place it somewhere on the island. We do this by simply selecting a random point (as `x` and `y`) within the island's bounding box, and checking to see it is ok to place the player there. So long as there are no other entities occupying that space, and the map point is within the range of shallows (MT_SHALLOWS) and normal land (MT_LAND), we can set the player location (again, the player isn't allowed to cross the sea, forests, or mountains, so we have to ensure that we start them in a location where they can walk).

With that location set, we set the current Map's `player` pointer as the created adventurer (`e`). Each map will have its own adventurer entity; we don't teleport the player object around themselves, since our game doesn't require us to do such a thing (in SDL2 Rogue, we did this, as we needed to retain the entity's state - in this game, we can get away with multiple player instances). It also save on micromanaging the player state.

With that done, we can now move over to player.c (in the game directory). This is a new compilation unit that will handle the player themselves, dealing with the input, movement, and such.

Starting with initPlayer:


void initPlayer(void)
{
	moveTimer = 0;

	speed = 6;
}

We're setting up two variables here, moveTimer and `speed`. moveTimer will be used to add a delay between movements, as we saw with the camera. `speed`, on the other hand, will determine the reset value of moveTimer whenever we move. We're setting this as a variable, rather than a constant, so that we can allow the player to accelerate when they walk around. This is done in order to give the user more control over the movement. When we first move the robot, the movement will be slow, but will get faster the longer we hold down the movement controls. This will help later, when we want to align ourselves with objects in the world; moving too fast will make this quite frustrating!

Now over to doPlayer, which is the function that will handle controlling the player:


void doPlayer(void)
{
	int dx, dy;

	moveTimer = MAX(moveTimer - app.deltaTime, 0);

	dx = dy = 0;

	if (moveTimer == 0)
	{
		if (app.keyboard[SDL_SCANCODE_W])
		{
			dy = -1;
		}

		if (app.keyboard[SDL_SCANCODE_S])
		{
			dy = 1;
		}

		if (app.keyboard[SDL_SCANCODE_A])
		{
			dx = -1;
		}

		if (app.keyboard[SDL_SCANCODE_D])
		{
			dx = 1;
		}

		if (dx != 0 || dy != 0)
		{
			if (dx != 0)
			{
				move(dx, 0);
			}

			if (dy != 0)
			{
				move(0, dy);
			}

			moveTimer = speed;

			speed = MAX(MAX_SPEED, speed - (0.2 * app.deltaTime));
		}
		else
		{
			speed = MIN_SPEED;
		}
	}
}

This is a pretty standard movement handling function, testing the state of our WASD control scheme, to see how we should move. If movement requested, we'll delegate to the `move` function, passing over `dx` and `dy`. Note that we're handling `dx` and `dy` separately, so that we can slide off walls, etc. Once the player has moved, we'll set moveTimer to the value of `speed`. Next, we'll decrease the value of `speed`, so that the next movement will happen faster. We're capping this to MAX_SPEED (defined at the top of player.c). If no movement is detected, we'll set `speed` back to MIN_SPEED.

Next up, we have the `move` function itself:


static void move(int dx, int dy)
{
	if (!moveEntity(dx, dy))
	{
		moveOverworld(dx, dy);
	}
}

Right now, we're calling two other functions: moveEntity and moveOverworld. What we're doing here is first checking for movements against other entities. If there aren't any entity interactions (that could block the player movement), we'll call moveOverworld. We'll quickly look at moveEntity next:


static int moveEntity(int dx, int dy)
{
	Entity *e;
	int     x, y;

	x = game.map->player->x + dx;
	y = game.map->player->y + dy;

	e = getEntityAt(x, y);

	if (e != NULL)
	{
		if (e->touch != NULL)
		{
			e->touch(e, game.map->player);
		}

		return e->solid;
	}

	return 0;
}

As expected, we're testing if there is an entity at the location the player is about to enter (`x`, `y`). If so, we'll call the other entity's `touch` function (to invoke that entity's interaction against the player). We'll then return whether the entity is `solid`. If it is, the call to moveOverworld in move won't occur, since this entity is occupying the space we wish to move to. Otherwise, we'll return 0. Nothing out of the ordinary, then.

Finally, we have moveOverworld:


static void moveOverworld(int dx, int dy)
{
	int t, x, y;

	x = MIN(MAX(game.map->player->x + dx, 0), WORLD_WIDTH - 1);
	y = MIN(MAX(game.map->player->y + dy, 0), WORLD_HEIGHT - 1);

	t = game.map->data[x][y] / TILE_TYPE_RANGE;

	if (t >= MT_SHALLOWS && t <= MT_LAND)
	{
		game.map->player->x = x;
		game.map->player->y = y;
	}
}

Another pretty standard function - this simply deals with moving the player against the world. Notice something, though - when considering the tile type the player wants to enter (`t`), we're dividing the map data point by TILE_TYPE_RANGE. This is so we can reduce values of 1, 6, 14, 15, 43, 48, etc. down to 0, 1, 4, etc., thus matching our MT_ enum. Remember that we decorated the overworld earlier, increasing the value of the tiles at the map points by 10, and adding an additional value up to 9. We therefore need to reverse this to check the tile type.

And that's really all we need in order to insert our player into the game. As we finish up, we'll look at the updates we've made elsewhere to support it. First, to overworld.c, and initOverworld:


void initOverworld(void)
{
	game.overworld.seed = rand();

	// snipped

	initEntities();

	initPlayer();

	// snipped
}

We're calling initEntities and initPlayer. Next, the update to `logic`:


static void logic(void)
{
	// snipped

	doPlayer();

	doEntities();

	doCamera();
}

We're calling doPlayer and doEntities. Lastly, we've updated `draw`:


static void draw(void)
{
	drawMap(tiles);

	drawEntities();
}

We're calling drawEntities here now, after we render the map.

A tweak to doCamera in camera.c was also required, to track the player:


void doCamera(void)
{
	game.map->camera.x = game.map->player->x - (MAP_RENDER_WIDTH / 2);
	game.map->camera.y = game.map->player->y - (MAP_RENDER_HEIGHT / 2);

	game.map->camera.x = MIN(MAX(game.map->camera.x, 0), game.map->width - MAP_RENDER_WIDTH);
	game.map->camera.y = MIN(MAX(game.map->camera.y, 0), game.map->height - MAP_RENDER_HEIGHT);
}

Pretty standard stuff, with the camera being centered over the player.

Before we conclude, remember that our entity handling will use the context of the current Map, in game. If we take a look at the getEntityAt function, we can see this:


Entity *getEntityAt(int x, int y)
{
	Entity *e;

	for (e = game.map->entityHead.next; e != NULL; e = e->next)
	{
		if (!e->hidden && e->x == x && e->y == y)
		{
			return e;
		}
	}

	return NULL;
}

We're checking the entities in the current map (Game's `map` is a pointer). This will be the case when we call doEntities and drawEntities, as well as various other entity handling functions. We'll see how this is put to further use when we get to generating towns. It essentially means we're working with a state machine. For now, Game's `map` will always point at the Overworld.

Another part done. But we're stuck on a single island, with no means of reaching the others. What we need is a boat, to cross the vast seas. This is something we'll add into our next part. Since we already have entity interactions in place, this will be very easy to do. We'll also be stranding the player on the smallest island, as an incentive to get out and explore (they'll also need to find the boat!).

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle:

From itch.io

Mobile site