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 Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 5: Collectables

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

Introduction

A fundamental aspect of our game will be the ability to earn cash rewards from defeating enemies. While we'll be awarding the player catnip (the currency) for each enemy defeated, we'll also be releasing collectables - catnip, ammo, and health powerups. These will float around for a few seconds before vanishing.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-05 to run the code. You will see a window open like the one above. Use the WASD control scheme to move the fighter around. Hold J to fire your main guns. Defeat the enemies to have them drop various collectables. To pick them up, simply fly into them. Notice how they flicker and vanish after a few seconds if they are not collected. Once you're finished, close the window to exit.

Inspecting the code

Like a lot of things now, adding in collectables is a fairly simple task. We've got various files to add and modify, but overall it's nice and easy.

So, let's start with defs.h:


enum
{
	CT_CATNIP,
	CT_HEALTH,
	CT_AMMO
};

We've add in a bunch of new enums. These will represent the type of collectable (CT being short of Collectable Type). CT_CATNIP will represent catnip, CT_HEALTH a health powerup, CT_AMMO an ammo powerup.

As one of our powerups gives us more ammo, we've added a #define for that purpose:


#define MAX_KITE_AMMO   8

We're not using ammo just yet, but we're going to make sure now that our collectable doesn't give us more ammo than MAX_KITE_AMMO.

Now let's look at structs.h, where we've made a new additions and updates:


struct Collectable
{
	int          type;
	double       x;
	double       y;
	double       dx;
	double       dy;
	double       health;
	int          value;
	AtlasImage  *texture;
	Collectable *next;
};

We've created a new struct to represent our collectable. By now, the meaning of most fields will be clear, so we'll skip ahead to the update to Stage:


typedef struct
{
	//snipped
	Collectable collectableHead, *collectableTail;
	double      engineEffectTimer;
	int         numActiveEnemies;
	int         hasCollectables;
	Entity     *player;
	PointF      camera;
} Stage;

We've added in a two fields, collectableHead and collectableTail, to act as the linked list for our collectables. Finally, we've updated Game:


typedef struct
{
	int catnip;
	struct
	{
		int ammo;
		int damage;
		int reload;
		int output;
	} kite;
} Game;

We've added a `catnip` field, as well an `ammo` field within the `kite` struct.

With that prepared, we do dig into the new collectables.c compilation unit. This will be very straightforward, as it adheres with the design and coding patterns we've been adopting the whole way through this series.

Starting with initCollectables:


void initCollectables(void)
{
	memset(&stage.collectableHead, 0, sizeof(Collectable));

	stage.collectableTail = &stage.collectableHead;

	if (healthTexture == NULL)
	{
		healthTexture = getAtlasImage("gfx/collectables/health.png", 1);
		catnipTexture = getAtlasImage("gfx/collectables/catnip.png", 1);
		ammoTexture = getAtlasImage("gfx/collectables/ammo.png", 1);
	}
}

A very standard init function. We're setting up our linked list for the collectables, and loading in some textures.

On to the next function, doCollectables:


void doCollectables(void)
{
	Collectable *c, *prev;

	prev = &stage.collectableHead;

	for (c = stage.collectableHead.next; c != NULL; c = c->next)
	{
		doCollectable(c);

		if (c->health <= 0)
		{
			prev->next = c->next;

			if (c == stage.collectableTail)
			{
				stage.collectableTail = prev;
			}

			free(c);

			c = prev;
		}

		prev = c;
	}
}

Again, a very standard function that is processing a linked list. For each one of our collectables, we're calling doCollectable. A collectable is removed from the game when it's `health` is 0 or less (such as when it is touched by the player or naturally expires).

All standard stuff so far. Okay, let's see what doCollectable does:


static void doCollectable(Collectable *c)
{
	Fighter *f;

	c->x += c->dx * app.deltaTime;
	c->y += c->dy * app.deltaTime;

	c->health -= app.deltaTime;

	if (collision(c->x, c->y, c->texture->rect.w, c->texture->rect.h, stage.player->x, stage.player->y, stage.player->texture->rect.w, stage.player->texture->rect.h))
	{
		switch (c->type)
		{
			case CT_CATNIP:
				game.catnip += c->value;
				break;

			case CT_HEALTH:
				f = (Fighter *)stage.player->data;
				f->health = MIN(f->health + 5, f->maxHealth);
				break;

			case CT_AMMO:
				game.kite.ammo = MIN(game.kite.ammo + 1, MAX_KITE_AMMO);
				break;

			default:
				break;
		}

		c->health = 0;
	}
}

Firstly, we're moving the collectable, by updating its `x` and `y` by it's `dx` and `dy`. We're also decreasing its `health`. Next, we're testing to see if it has collided with the player. If so, we'll work out what `type` of collectable this is, and respond accordingly. If it's CT_CATNIP, we'll increase game's `catnip` by the `value` of the collectable. If it's CT_HEALTH, we'll award the player 5 extra health points. If it's CT_AMMO, we'll increase Game's `kite`'s `ammo` by 1, but not allow it to go higher than MAX_KITE_AMMO. With that done, we'll set the collectable's `health` to 0, so that it is removed from the game.

A straightforward function - we're just moving the collectable around, preparing to time it out, and checking to see if the player has collected it.

Next, let's look at drawCollectables:


void drawCollectables(void)
{
	Collectable *c;
	int          x, y;

	for (c = stage.collectableHead.next; c != NULL; c = c->next)
	{
		x = c->x - stage.camera.x;
		y = c->y - stage.camera.y;

		if ((collision(x, y, c->texture->rect.w, c->texture->rect.h, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) && (c->health > FPS * 2 || (int)c->health % 10 < 5))
		{
			blitAtlasImage(c->texture, x, y, 0, SDL_FLIP_NONE);
		}
	}
}

No real surprises here. We're drawing the collectable if it's on screen. One thing we're doing extra is checking if the collectable's `health` is greater than 2 seconds, or if the modulo 10 of the `health` is less than 5. What this means is that if the collectable has at least 2 seconds of health left, it will always draw. However, if not we'll only draw it based on the value of its `health`. This will cause it to flicker when it's close to expiry, giving us a hint that we need to grab it soon.

That's our processing and drawing done. Now, let's look at the other functions we have. Starting with initCollectable:


static Collectable *initCollectable(int type, int x, int y, AtlasImage *texture)
{
	Collectable *c;

	c = malloc(sizeof(Collectable));
	memset(c, 0, sizeof(Collectable));
	stage.collectableTail->next = c;
	stage.collectableTail = c;

	c->x = x;
	c->y = y;
	c->type = type;
	c->dx = rand() % 250 - rand() % 250;
	c->dy = rand() % 250 - rand() % 250;
	c->dx *= 0.01;
	c->dy *= 0.01;
	c->health = (FPS * 7) + rand() % FPS;
	c->texture = texture;

	return c;
}

This function is basically a factory for our Collectables, settings up all the common fields, attributes, and behaviour. We're passing in the `type`, the originating position (`x` and `y`), and the `texture` to use. Next, we're mallocing a Collectable, adding it to our list, and setting its values. Every collectable created will move in a random direction, at a random velocity (determined by its `dx` and `dy` values), and will live for between 6 and 7 seconds.

If we look at the dropHealth function, we can see how it is used:


void dropHealth(int x, int y)
{
	initCollectable(CT_HEALTH, x, y, healthTexture);
}

As expected, we're passing over CT_HEALTH, as well as the starting position, and the health texture. The position values are passed into the dropHealth function itself.

dropCatnip follows, and takes a somewhat different approach:


void dropCatnip(int x, int y, int amount)
{
	Collectable *c;
	int          value;

	while (amount > 0)
	{
		if (amount > 5)
		{
			value = rand() % amount;
			value = MAX(value, 5);
		}
		else
		{
			value = amount;
		}

		c = initCollectable(CT_CATNIP, x, y, catnipTexture);
		c->value = value;

		amount -= value;
	}
}

The idea behind this function is to create a number of catnip collectables, based on the `amount` of catnip passed into the function. The function will enter a while-loop, creating catnip collectables with a minimum value of 5 (unless `amount` is 5 or less, in which case it will be the remaining value). The amount assigned to the collectable will be deducted from the amount passed into the function, until it reaches 0. This means that our catnip collectables will all have random values, meaning that the player will have to collect them all to gain the full amount (and when being fired upon, this might not be possible!). This is done just to add some variety to the proceedings, rather than create a single catnip collectable of the value we request.

Finally, we have dropAmmo:


void dropAmmo(int x, int y)
{
	initCollectable(CT_AMMO, x, y, ammoTexture);
}

It's largely the same as the dropHealth function, except for the different arguements.

That's it for collectables.c. Now we can move on and see how it's all put together. If we head over to greebleLightFighter.c, we've updated the `die` function:


static void die(Entity *self)
{
	int x, y;

	x = self->x + (self->texture->rect.w / 2);
	y = self->y + (self->texture->rect.h / 2);

	addExplosions(x, y, 25);

	addDebris(x, y, 12);

	dropCatnip(self->x, self->y, rand() % 35);

	if (rand() % 4 == 0)
	{
		dropHealth(self->x, self->y);
	}

	if (rand() % 5 == 0)
	{
		dropAmmo(self->x, self->y);
	}

	self->dead = 1;
}

Whenever the enemy fighter is destroyed, it will drop between 0 and 34 catnip (via a called to dropCatnip). There's also a 1 in 4 chance that we're going to call dropHealth, and a 1 in 5 chance that we'll call dropAmmo. So, our fighter has a chance to drop all our collectables whenever it it destroyed.

The last thing we need to do is integrate our collectables code into stage.c. So, starting with initStage:


void initStage(void)
{
	// snipped

	initDebris();

	initCollectables();

	initEntity("player");

	background = loadTexture("gfx/backgrounds/default.jpg");

	addEnemyTimer = 0;

	app.delegate.logic = logic;
	app.delegate.draw = draw;
}

We're making the call to initCollectables. Next, we've updated doStage:


static void doStage(void)
{
	// snipped

	doDebris();

	doCollectables();

	doStarfield(stage.ssx * 0.75, stage.ssy * 0.75);

	doCamera();

	// snipped
}

We've added the call to doCollectables. And finally, in `draw`:


static void draw(void)
{
	// snipped

	drawBullets();

	drawCollectables();

	drawEffects();
}

We've added drawCollectables.

That was easy! And the nice thing about this system, as we'll see later, is that we can expand it to include other items that the player can pick up, with ease.

But what about the health, catnip, and ammo values that these collectables are affecting? It would be good to know what we've picked up, and how much of it. So, in the next part we're going to introduce our HUD, that will also incorporate elements such as messages, health, and ammo.

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