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


A North-South Divide

For over a hundred years, messenger Duncan has wandered the world, searching for the missing pieces of an amulet that will rid him of his curse; a curse that has burdened him with an extreme intolerance of the cold, an unnaturally long life, and the despair of watching all he knew and loved become lost to the ravages of time. But now, Duncan is close to the end of his long quest.

Click here to learn more and read an extract!

« Back to tutorial listing

— Making a 2D split screen game —
Part 10: Collectables

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

Introduction

There's lots of things in our game already, including a HUD and some nice effects. But what this game is missing (well, one thing..!) is the ability to collect power-ups and other such items. In SDL2 Versus, we'll had "pods" that the players can collect, that will either score them points, restore their health, etc. These pods will randomly appear in our zone, and be a valuable thing that the players can battle over.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus10 to run the code. You will see a window open like the one above, with each player on either side of our zone. Use the default controls (or, at your option, copy the config.json file from a previous tutorial, to use that - remember to exit the game before copying the replacement file). Play the game as normal. Notice how small items appear on screen, that the player can guide their ships towards to collect. These can earn the player points, restore their health, restore their rockets, or grant them an energy shield for a short time. The pods will last for a short time before vanishing. When points are earned, the HUD will update accordingly for the player. Once you're finished, close the window to exit.

Inspecting the code

Our Pods are very much like power-ups in many other games (and some of our past tutorials). They appear on screen, and will grant the player the relevant ability or resource if the player makes contact with them. Our Pods are simple entities, and since we already have code in place for entity-to-entity collision checks, putting in these Pods is very straightforward.

We'll start first with defs.h:


enum
{
	ET_NONE,
	ET_PLAYER,
	ET_POD
};

To begin with, we've added a new enum to our entity types. ET_POD will be used to specify that this entity is an item.

Next, we've created a new enum:


enum
{
	PT_SCORE,
	PT_HEALTH,
	PT_SHIELD,
	PT_AMMO,
	PT_MAX
};

This will represent our pod types (PT). We'll come to these in a little bit. Note that our enum values are named after the type of Pod that they will represent. Notice that we have a "max" entry. This is so that we can create an array to hold our pods models.

Next, over to structs.h, where we've made some updates and additions. Starting with Player:


typedef struct
{
	uint8_t num;
	int     score;
	int     health;
	double  shield;
	double  reload;
	double  gunHeat;
	int     rockets;
	double  spinTimer;
	Entity *spinInflictor;
} Player;

We've added a new variable here, called `score`. Predictably, this will hold the score for the player.

Next up, we have the Pod struct:


typedef struct
{
	uint8_t type;
	double  thinkTime;
	double  bob;
	double  health;
} Pod;

This struct is used to hold all the data for a Pod. `type` is the type of pod this is (PT). thinkTime is used to control how long it will be before this Pod takes its next action (we don't want all our Pod to be doing heavily processing at the same time, or all the time, for that matter). `bob` is a variable used to control the gentle bobbing of the Pod on screen, while `health` is how long the Pod will live for.

That's our definitions of our Pod. We can now move over to pod.c, where the logic for an individual Pod entity lives. There's quite a few function here, but nothing tricky to understand. Let's start with initPod:


void initPod(int x, int y, int type)
{
	Entity *e;
	Pod    *p;

	if (models[0] == NULL)
	{
		models[PT_SCORE] = loadModel("data/models/scorePod");
		models[PT_HEALTH] = loadModel("data/models/healthPod");
		models[PT_SHIELD] = loadModel("data/models/shieldPod");
		models[PT_AMMO] = loadModel("data/models/ammoPod");
	}

	p = malloc(sizeof(Pod));
	memset(p, 0, sizeof(Pod));

	p->startY = y;
	p->health = FPS * (300 + rand() % 300) * 0.1;
	p->bob = rand() % 360;
	p->type = type;

	e = spawnEntity(ET_POD);
	e->position.x = x;
	e->position.y = y;
	e->radius = 12;
	e->model = models[type];
	e->data = p;

	e->tick = tick;
	e->draw = draw;
	e->touch = touch;
}

This function creates of a Pod at the specified location (`x` and `y`), and of the specified type (`type`). We start by loading the various models that our Pods will use, if need be (models is static in pod.c, and is an array of PT_MAX in size), then create the Pod data itself (`p`). We set the various attributes, including giving the Pod a random amount of `health` (between 30 and 60 seconds). We create the actual entity (`e`) with the ET_POD type, and set the `model` from the appropriate index in the `models` array (our models array aligns with the type of pods available).

All as expected. Now, over to `tick`:


static void tick(Entity *self)
{
	Pod *p;

	p = (Pod *)self->data;

	p->health -= app.deltaTime;

	if (p->health <= 0)
	{
		self->dead = 1;
	}

	p->thinkTime -= app.deltaTime;

	if (p->thinkTime <= 0)
	{
		p->thinkTime = FPS / 4;

		lookForPlayers(self, p);
	}

	self->position.x += self->dir.x * app.deltaTime;
	self->position.y += self->dir.y * app.deltaTime;

	p->bob += 0.1 * app.deltaTime;
}

Our `tick` function will slowly deplete our Pod's `health`, setting its `dead` flag to 1 if its `health` drops to 0 or less. We're also decreasing the Pod's thinkTime. If the thinkTime is 0 or less, we'll be calling a function named lookForPlayers. This is what causes the Pod to quickly move towards the closest player, so that the player doesn't need to perfectly align themselves with the Pod in order to pick it up. The Pod will do this every 1/4 of a second. We're also updating the Pod's `position`, based on its `dir`, to make it move, and also updating the `bob` value. We'll see more on the `bob` value when we come to drawing.

First, let's look at the lookForPlayers function:


static void lookForPlayers(Entity *self, Pod *p)
{
	int     i;
	Entity *other;
	double  dx, dy;

	for (i = 0; i < NUM_PLAYERS; i++)
	{
		other = zone.players[i];

		if (hypot(self->position.x - other->position.x, self->position.y - other->position.y) < 100)
		{
			normalizeVector(other->position.x, other->position.y, self->position.x, self->position.y, &dx, &dy);

			self->dir.x = dx * 12;
			self->dir.y = dy * 12;

			p->thinkTime = FPS / 8;
		}
	}
}

This function is pretty easy to understand. The Pod will loop through both our players, looking for one closer than 100 pixels (with help from the hypot function). If one is found, we'll set the pod's `dir` to make it move quickly towards the player (using normalizeVector, and a `d`x and `dy` variable pair). We'll update the thinkTime here to 1/8th of a second, to make it react much faster, now that it is homing in on a player; we want the item to be collected as fast as possible.

One thing about this function is that it will favour the second player, in situations where both players are nearby. But, to be honest, if both players were that close, there's a good chance there is more to worry about..! If we wanted to make this fairer, we could calculate the distances to both players, and then choose the nearest one to move towards. Again, given the proximity, this isn't something we need concern ourselves with.

Now, let's move on to `draw`:


static void draw(Entity *self, SDL_FPoint *camera)
{
	Pod       *p;
	SDL_FPoint drawPosition;

	p = (Pod *)self->data;

	if (p->health >= FPS * 2 || ((int)p->health % 10 > 5))
	{
		drawPosition = self->position;
		drawPosition.x -= camera->x;
		drawPosition.y -= camera->y;
		drawPosition.y += sin(p->bob) * 8;

		drawModel(self->model, drawPosition, 0);
	}
}

Our `draw` function is pretty simple. We're first testing if the Pod still has at least two seconds left to live, or if the `health` value's modulo of 10 is greater than 5, and drawing our model as normal, taking the camera position into account. The reason for the health check before hand is that this allows us to make the Pod start to rapidly blink if its `health` is running low, signalling to the players that it is about to expire. Notice also that when drawing our Pod, we're updating the drawPosition's `y` value by the sin of `bob` (multiplied by 8). This means our Pod will move up and down in a small area, to draw the players' attentions. This is just an aesthetic thing, and our Pod doesn't actually move anywhere.

Finally, we have the `touch` function, which is the most important function of all:


static void touch(Entity *self, Entity *other)
{
	Pod    *p;
	Player *pl;

	p = (Pod *)self->data;

	if (other->type == ET_PLAYER)
	{
		pl = (Player *)other->data;

		switch (p->type)
		{
			case PT_SCORE:
				pl->score += 25;
				break;

			case PT_HEALTH:
				pl->health = MAX_PLAYER_HEALTH;
				break;

			case PT_SHIELD:
				pl->shield = MAX_PLAYER_SHIELD;
				break;

			case PT_AMMO:
				pl->rockets = MAX_ROCKETS;
				break;

			default:
				break;
		}

		self->dead = 1;
	}
}

The first thing we do is check if the thing that has touched the Pod is a Player (ET_PLAYER); only Players can collect Pods, after all! next, we check what `type` of Pod this is, and respond appropriately. A score pod (PT_SCORE) will grant the player 25 points. A health pod (PT_HEALTH) will restore the Player's health to full. A shield pod (PT_SHIELD) will grant the Player a shield. An ammo Pod (PT_AMMO) will restore the Player's supply of rockets.

With that done, we set the Pod's `dead` flag to 1, to remove it from the game.

So, all as expected.

The next new file we'll look at is pods.c (with a plural, since this file is for handling adding pods to our zone). First up, we have initPods:


void initPods(void)
{
	randomPodTimer = 0;
}

We're setting a variable called randomPodTimer to 0. This variable controls how often we add new pods. We can see this in action in doPods:


void doPods(void)
{
	randomPodTimer -= app.deltaTime;

	if (randomPodTimer <= 0)
	{
		addRandomPod();

		randomPodTimer = FPS * (1 + rand() % 10);
	}
}

We start be decreasing the value of randomPodTimer. When it falls to 0 or less, we'll call addRandomPod, and set randomPodTimer to a value between 1 and 10 seconds, for when a new pod will be created.

The addRandomPod function itself is easy to enough to understand:


void addRandomPod(void)
{
	int x, y, i, t;

	do
	{
		x = (zone.bounds.x + 32);
		x += rand() % (zone.bounds.w - (zone.bounds.x + 64));

		y = (zone.bounds.y + 32);
		y += rand() % (zone.bounds.h - (zone.bounds.y + 64));
	} while (!canAddPod(x, y));

	i = rand() % 100;

	if (i < 75)
	{
		t = PT_SCORE;
	}
	else if (i < 90)
	{
		t = PT_AMMO;
	}
	else if (i < 95)
	{
		t = PT_SHIELD;
	}
	else
	{
		t = PT_HEALTH;
	}

	initPod(x, y, t);
}

The function sets up a do-while loop, and attempts to choose a position within our zone where it can add a pod. We do this by making a call to canAddPod, passing over our randomly selected `x` and `y` coordinates. Once we've found somewhere to add our Pod, we'll randomly select its `type` (score Pods are most common, health pods the rarest), and call initPod, passing over the `x` and `y` values, as well as the Pod type (`t`).

All nice and simple. The canAddPod is the last function in pods.c:


static uint8_t canAddPod(int x, int y)
{
	Triangle *t;
	int px, py;

	for (t = getWorldTriangles(); t != NULL; t = t->next)
	{
		for (px = -1 ; px <= 1; px++)
		{
			for (py = -1 ; py <= 1 ; py++)
			{
				if (pointInTriangle(x + (px * 32), y + (py * 32), t->points[0].x, t->points[0].y, t->points[1].x, t->points[1].y, t->points[2].x, t->points[2].y))
				{
					return 0;
				}
			}
		}
	}

	return 1;
}

This function accepts `x` and `y` variables as coordinates, and then loops through all the triangles in our zone, testing to see if any number of points that our Pod might occupy are inside of a triangle. We're using two inner loops for this: `px` and `py` each go from -1 to 1, and are multiplied by 16, adding to the `x` and `y` passed into the function. This very roughly lets us know if the Pod might appear either inside a triangle, or is close to it. Should the area be unoccupied, we can add our Pod there.

That's about it for our Pod logic. It's likely as you would expect, based on the tutorials that have preceeded this one.

Before we finish up, let's quickly look at where we're using the functions from pods.c. Over to zone.c, we've first updated initZone:


void initZone(void)
{
	memset(&zone, 0, sizeof(Zone));

	initEntities();

	initBullets();

	initParticles();

	initPods();

	initWorld();

	// snipped
}

We're calling initPods here. Next, we've updated `logic`:


static void logic(void)
{
	doBullets();

	doEntities();

	doPods();

	// snipped
}

Here, we're calling doPods. We've no need to update `draw`, since our Pods are entities.

And finally, we've updated hud.c:


static void drawBottomBars(void)
{
	int     i;
	Player *p;
	char    text[16];

	drawRect(0, SCREEN_HEIGHT - 25, SCREEN_WIDTH, 30, 0, 0, 0, 200);

	app.font.scale = 0.5;

	for (i = 0; i < NUM_PLAYERS; i++)
	{
		p = (Player *)zone.players[i]->data;

		// snipped

		sprintf(text, "Score: %05d", p->score);
		drawText(text, (SCREEN_WIDTH / 2) * (i + 1) - PADDING, SCREEN_HEIGHT - 28, 255, 255, 255, TEXT_ALIGN_RIGHT, 0);
	}

	app.font.scale = 1;
}

We're now drawing the Players' score! So, whenever we collect score Pods, we'll see this value increase. See, I told you the HUD would be useful!

Another great step forward. Our Pods allow the players to score points, replenish their health, ammo, and gain a shield. Our game will let our players win the matches in a number of different ways, such as by scoring the most points, or be defeating their opponent in combat, and stripping them of all their lives. The Pods help us to greatly enhance this.

But speaking of goals, there's currently nothing for the players to aim towards (by which I mean, there is no point to the game!). We can endlessly die and be recreated, and pick up lots of pods, but to what end..? Well, in the next part, we're going to introduce the concept of goals, so that the players have a reason to compete with one another.

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