« Back to tutorial listing

— 2D Shoot 'Em Up Tutorial —
Part 12: Points Pods

Introduction

Note: this tutorial builds upon the ones that came before it. If you aren't familiar with the previous tutorials in this series you should read those first.

We can shoot the enemies to destroy them, but right now they just vanish. That's no fun. How about we make them explode, instead? Extract the archive, run cmake CMakeLists.txt, followed by make to build. Once compiling is finished type ./shooter12 to run the code.

A 1280 x 720 window will open, with a colorful background. A spaceship sprite will also be shown, as in the screenshot above. The ship can now be moved using the arrow keys. Up, down, left, and right will move the ship in the respective directions. You can also fire by holding down the left control key. Enemies (basically red versions of the player's ship) will spawn from the right and move to the left. Shoot enemies to destroy them. Enemies can fire back, so you should avoid their shots. Score points by collect points pods released from destroyed enemy ships. Close the window by clicking on the window's close button.

Inspecting the code

To make the game a little more challenging, we're going to make it so that the player only gains points by collects points pods that are released by destroying enemy ships. The means that the player will be forced to negotiate enemy fire while hunting them down. Let's begin with structs.h:


typedef struct {
	...
	Entity pointsHead, *pointsTail;
	...
} Stage;

We've added a linked list to Stage, to allow it to hold our points pods. Nothing more to say here. Moving on and we see that, as always, the bulk of the changes have been made in stage.c. Starting with initStage:


void initStage(void)
{
	...
	pointsTexture = loadTexture("gfx/points.png");
	...

We start by loading the texture that we'll be using to draw our point pods with (pointsTexture). Then, we reset our points pods linked lists in resetStage:


static void resetStage(void)
{
	...
	while (stage.pointsHead.next)
	{
		e = stage.pointsHead.next;
		stage.pointsHead.next = e->next;
		free(e);
	}

	...
	stage.pointsTail = &stage.pointsHead;
	...
}

With that done, we add a new function call to logic, telling it to call doPointsPods.


static void logic(void)
{
	...
	doPointsPods();
	...

This function will handle our points pods. We'll see more on this later on. We next update bulletHitFighter:


static int bulletHitFighter(Entity *b)
{
	...
	if (e == player)
	{
		playSound(SND_PLAYER_DIE, CH_PLAYER);
	}
	else
	{
		addPointsPod(e->x + e->w / 2, e->y + e->h / 2);

		playSound(SND_ALIEN_DIE, CH_ANY);
	}
	...

This update will call a new function called addPointsPod. It will pass over the x and y coordinates that the points pod should spawn from. Note that we're passing over the destroyed fighter's midpoint. Also note that we're only doing this for enemy fighters, so that we don't see the player release a points pod when they are destroyed.

Now let's consider our new doPointsPods routine:


static void doPointsPods(void)
{
	Entity *e, *prev;

	prev = &stage.pointsHead;

	for (e = stage.pointsHead.next ; e != NULL ; e = e->next)
	{
		if (e->x < 0)
		{
			e->x = 0;
			e->dx = -e->dx;
		}

		if (e->x + e->w > SCREEN_WIDTH)
		{
			e->x = SCREEN_WIDTH - e->w;
			e->dx = -e->dx;
		}

		if (e->y < 0)
		{
			e->y = 0;
			e->dy = -e->dy;
		}

		if (e->y + e->h > SCREEN_HEIGHT)
		{
			e->y = SCREEN_HEIGHT - e->h;
			e->dy = -e->dy;
		}

		e->x += e->dx;
		e->y += e->dy;

		if (player != NULL && collision(e->x, e->y, e->w, e->h, player->x, player->y, player->w, player->h))
		{
			e->health = 0;

			stage.score++;

			highscore = MAX(stage.score, highscore);

			playSound(SND_POINTS, CH_POINTS);
		}

		if (--e->health <= 0)
		{
			if (e == stage.pointsTail)
			{
				stage.pointsTail = prev;
			}

			prev->next = e->next;
			free(e);
			e = prev;
		}

		prev = e;
	}
}

It appears that plenty is going on here, but it's a little more straightforward than that. We step through our linked list, handling each points pod in turn. First, we check to see if the points pod's x has gone off screen. If so, we reverse the pod's dx. We do the same check with the pod's y, reversing the dy if needed. This will cause the pod to bounce, making things more interesting. We then add the pod's dx and dy to its x and y respectively, to move it. With the movement done, we then check to see if the pod has made contact with the player. We first ensure player isn't NULL and then call our collision function, passing over the player's and pod's x, y, w, and h. If contact is made, the set the pod's health to 0, increment stage.score, update highscore if needed, and finally play a sound. At all times, a points pod's health will be decremented and the pod deleted if health hits 0 or less.

That should actually all be quite easy to understand. Equally, addPointsPod should be very simple:


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

	e = malloc(sizeof(Entity));
	memset(e, 0, sizeof(Entity));
	stage.pointsTail->next = e;
	stage.pointsTail = e;

	e->x = x;
	e->y = y;
	e->dx = -(rand() % 5);
	e->dy = (rand() % 5) - (rand() % 5);
	e->health = FPS * 10;
	e->texture = pointsTexture;

	SDL_QueryTexture(e->texture, NULL, NULL, &e->w, &e->h);

	e->x -= e->w / 2;
	e->y -= e->h / 2;
}

Our points pod is an Entity. We malloc one and add it to our linked list, then set its x and y to the coordinates passed into the function. We then set the pod's dx to be a random value between 0 and -4. This means that in most cases the pod will move from right to left. We then set the pod's dy to a random between -5 and +5, causing it to move up or down the screen at a random speed. Remember that when the pods touch the sides of the screen their velocity will be reversed, causing them to bounce. The pod's health is set to FPS * 10, making them live for 10 seconds before vanishing. We next set the pod's texture to pointsTexture, grab the width and height of the texture, setting them to the pod's w and h using SDL_QueryTexture, allowing our collision detection routine to work. Finally, we make one small adjustment to the pod's x and y, shifting each one by half the pod's w and h to better center them.

Now for drawing. This won't be too complicated, at all. We update our main draw function, adding a call to drawPointsPods:


static void draw(void)
{
	drawBackground();

	drawStarfield();

	drawPointsPods();
	...

Note that we're drawing the points pods after drawStarfield, quite early on in the whole scene. This means that the points pods won't obscure things like bullets, explosions, and fighters. The last update in stage.c is the actual drawPointsPods function:


static void drawPointsPods(void)
{
	Entity *e;

	for (e = stage.pointsHead.next ; e != NULL ; e = e->next)
	{
		blit(e->texture, e->x, e->y);
	}
}

Very, very simple. We just step through our linked list and draw all the pods.

Before we wrap up, let's look at a couple of other things that have changed. defs.h has seen a couple of new additions:


enum
{
	...
	CH_POINTS
};

enum
{
	...
	SND_POINTS,
	...

We add a new sound channel to play our points sound through (so that it doesn't get cut off by things like explosions and the sound of firing). We also declare SND_POINTS into which we'll load our sounds. This can be seen in sound.c:


static void loadSounds(void)
{
	...
	sounds[SND_POINTS] = Mix_LoadWAV("sound/342749__rhodesmas__notification-01.ogg");
}

And that's a wrap! The game is now more fun and challenging to play. It's no longer possible to rack up dozens of points, making achieving a highscore a matter of skill. It would be good to log all these attempts at a highscore. In the next tutorial, we'll do just that: introduce a highscore table.

Exercises

  • Release more points pods, some worth more than others (you might want new textures for this).
  • Allow the points pod to be shot and destroyed.
  • Make a points pod's value decrease the older it gets (again, you might want new textures for this).

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:

Desktop site