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
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 Red Road

For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ...

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 2: Main guns

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

Introduction

We've made a good start so far. We can fly our ship around, and see the starfield and background scroll along with it. What would be great to add in now is our weapons system. After all, this is a shoot 'em up. We're going to initally focus on the main guns, and add in the secondary weapons later on.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-02 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 gun. The KIT-E fighter will support a number of upgrades, including rate of fire, number of shots fired, and damage. To modify these, you can edit game.c (starting at line 15), recompile, and run again:

game.kite.reload = {1 - 20};
game.kite.output = {1 - 5};
game.kite.damage = {1 - 999};

Updating the values to one shown in the curly braces will modify the KIT-E's guns. A lower reload value will make the KIT-E fire faster. An output value between 1 and 5 will change the number of shots fired. damage will change the damage done per shot (although there is nothing to shoot right now). Obviously, you should use a literal number and not the curly braces shown here. When you're finished, close the window to exit.

Inspecting the code

Adding the ability to fire is something we've seen quite a few times before, and so we'll be sticking with the same forumla for issuing bullets. There are quite a few tweaks that need to be made to the code to enable this, but nothing out of the ordinary.

So, let's start with defs.h:


#define MIN_KITE_RELOAD 20

We've added in a define to represent the minimum reload value of the KIT-E's guns. Having this as a constant will prove useful later.

We've also added an enum:


enum
{
	SIDE_NONE,
	SIDE_CATS,
	SIDE_GREEBLE
};

The SIDE enum is something we've seen before, and will be used to determine to which side an entity belongs. In our game, aliens cannot shoot other aliens, and the player cannot harm any buddies that might also exist in the stage. We saw this used in both SDL2 Shooter and Battle Arena Donk.

On to the updates the structs.h. Starting with Fighter:


typedef struct
{
	double health;
	int    maxHealth;
	double dx, dy;
	double reload;
} Fighter;

We've added in a `reload` field, to handle the rate of fire.

Next, we've added in a Bullet struct:


struct Bullet
{
	double      x;
	double      y;
	double      dx;
	double      dy;
	double      health;
	int         damage;
	int         facing;
	AtlasImage *texture;
	Entity     *owner;
	void (*tick)(Bullet *b);
	void (*draw)(Bullet *b);
	Bullet *next;
};

Again a familiar object. The only new thing here is a `tick` function. This exists to support the various different weapon type, that will all exhibit different behaviours. We'll see this come into full use when we start on the optional weapons.

Of course, we've updated Stage:


typedef struct
{
	double  ssx, ssy;
	Effect  effectHead, *effectTail;
	Entity  entityHead, *entityTail;
	Bullet  bulletHead, *bulletTail;
	double  engineEffectTimer;
	Entity *player;
	PointF  camera;
} Stage;

We're supporting a Bullet linked list.

Finally, we've added in a Game struct:


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

Not a lot in here right now, except for the KIT-E's data. This struct will be greatly expanded in future parts.

Since we've seen the Game struct, we might as well discuss game.c quickly. There is only one function here - initGame:


void initGame(void)
{
	memset(&game, 0, sizeof(Game));

	game.kite.reload = MIN_KITE_RELOAD;
	game.kite.output = 1;
	game.kite.damage = 1;
}

We're defaulting our KIT-E's weapons systems here. As we'll see later, when we do things with our player's fighter, we'll often reference the `kite` struct in Game, as it's where we'll store the configuration, rather than embed it into a Fighter object. It allows us to work with a single point of truth as much as possible. initGame is called in the `main` (in main.c) function right now.

Moving over to fighters.c, we've updated the fighterTick function:


void fighterTick(Entity *e, Fighter *f)
{
	e->x += f->dx * app.deltaTime;
	e->y += f->dy * app.deltaTime;

	f->reload = MAX(f->reload - app.deltaTime, 0);
}

We're decreasing the value of the fighter's `reload` variable, limiting it to 0. When `reload` is 0, our fighter will be able to fire their guns.

Now over to player.c, where we've made a lot more changes. Starting with initPlayer:


void initPlayer(Entity *e)
{
	Fighter *f;

	f = malloc(sizeof(Fighter));
	memset(f, 0, sizeof(Fighter));

	e->side = SIDE_CATS;
	e->facing = FACING_RIGHT;
	e->data = f;
	e->texture = getAtlasImage("gfx/fighters/kit-e.png", 1);

	e->tick = tick;
	e->draw = draw;

	stage.player = e;
}

The player entity's `side` is set to SIDE_CATS, since he is on the side of the felines (in case you're wondering what the plot of our game is, we'll get to that in a later part - short version is that it's cats vs greebles).

We've also updated doPlayerControls:


static void doPlayerControls(Entity *self, Fighter *f)
{
	// snipped

	if (f->reload == 0 && isControl(CONTROL_FIRE))
	{
		fireGuns(self, f);
	}

	f->dx = MIN(MAX(f->dx, -MAX_FIGHTER_SPEED), MAX_FIGHTER_SPEED);
	f->dy = MIN(MAX(f->dy, -MAX_FIGHTER_SPEED), MAX_FIGHTER_SPEED);
}

We're now testing if the player's `reload` is 0 and that the fire control has been pressed (CONTROL_FIRE), and calling fireGuns if so.

The fireGuns function is where the interesting things happen:


static void fireGuns(Entity *self, Fighter *f)
{
	int dx, y;

	dx = self->facing == FACING_RIGHT ? 24 : -24;
	y = self->y + (self->texture->rect.h / 2);

	switch (game.kite.output)
	{
		case 1:
			addStandardBullet(self, game.kite.damage, y, dx, 0);
			break;

		case 2:
			addStandardBullet(self, game.kite.damage, y - 5, dx, 0);
			addStandardBullet(self, game.kite.damage, y + 5, dx, 0);
			break;

		case 3:
			addStandardBullet(self, game.kite.damage, y, dx, -2);
			addStandardBullet(self, game.kite.damage, y, dx, 0);
			addStandardBullet(self, game.kite.damage, y, dx, 2);
			break;

		case 4:
			addStandardBullet(self, game.kite.damage, y, dx, -2);
			addStandardBullet(self, game.kite.damage, y - 5, dx, 0);
			addStandardBullet(self, game.kite.damage, y + 5, dx, 0);
			addStandardBullet(self, game.kite.damage, y, dx, 2);
			break;

		case 5:
			addStandardBullet(self, game.kite.damage, y, dx, -4);
			addStandardBullet(self, game.kite.damage, y, dx, -2);
			addStandardBullet(self, game.kite.damage, y, dx, 0);
			addStandardBullet(self, game.kite.damage, y, dx, 2);
			addStandardBullet(self, game.kite.damage, y, dx, 4);
			break;

		default:
			break;
	}

	f->reload = game.kite.reload;
}

That's quite a big function, but one that's easy to understand. To begin with, we're assigning two variables: `dx`, that will determine the direction our bullet will travel, based on the direction the player is facing; and `y`, which will be the vertical position from where the bullet originates, based on the player's position, plus half the sprite's height. With that done, we're going to work out how many bullets we want to fire. Here is where we'll check the value of Game's `kite`'s `output`. We'll then call addStandardBullet the appropriate number of times, using a configuration for how we want the bullets to behave. We'll see how addStandardBullet works in a little while, but in summary our bullets will be fired as single, dual, or a spread format as the value of output increases. To the function, we're passing through the `dx` and `y` variables that we precalculated.

Now let's turn our attention to bullets.c. Starting with initBullets:


void initBullets(void)
{
	memset(&stage.bulletHead, 0, sizeof(Bullet));

	stage.bulletTail = &stage.bulletHead;

	standardBulletTextures[0] = getAtlasImage("gfx/bullets/greenPlasma.png", 1);
	standardBulletTextures[1] = getAtlasImage("gfx/bullets/redPlasma.png", 1);
}

Okay, we're preparing a linked list and loading some textures. Nothing special.

Turning next to doBullets:


void doBullets(void)
{
	Bullet *b, *prev;

	prev = &stage.bulletHead;

	for (b = stage.bulletHead.next; b != NULL; b = b->next)
	{
		b->x += b->dx * app.deltaTime;
		b->y += b->dy * app.deltaTime;
		b->health -= app.deltaTime;

		b->tick(b);

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

			if (b == stage.bulletTail)
			{
				stage.bulletTail = prev;
			}

			free(b);

			b = prev;
		}

		prev = b;
	}
}

Again, a standard processing loop. Here, we're moving the bullets (adding `dx` and `dy` to `x` and `y`), decreasing health, and calling the `tick` function. We're also removing dead bullets (`health` being 0 or less).

On to drawBullets:


void drawBullets(void)
{
	Bullet *b;

	for (b = stage.bulletHead.next; b != NULL; b = b->next)
	{
		b->draw(b);
	}
}

Okay, we're calling each bullet's `draw` function, nothing more. We'll see the draw and tick functions we've set shortly.

We also have a spawnBullet function:


static Bullet *spawnBullet(Entity *owner)
{
	Bullet *b;

	b = malloc(sizeof(Bullet));
	memset(b, 0, sizeof(Bullet));
	stage.bulletTail->next = b;
	stage.bulletTail = b;

	b->owner = owner;

	return b;
}

We're just creating a bullet, adding it to our linked list, setting the `owner` to the entity passed into the function, and returning the bullet. This is basically going to be a shared function to generate a bullet.

Now for the addStandardBullet function, that is being called when we fire the player's guns:


void addStandardBullet(Entity *owner, int damage, int y, int dx, int dy)
{
	Bullet *b;

	b = spawnBullet(owner);
	b->x = owner->x + (dx > 0 ? owner->texture->rect.w : 0);
	b->y = y;
	b->dx = dx;
	b->dy = dy;
	b->damage = damage;

	if (owner->side == SIDE_CATS)
	{
		b->texture = standardBulletTextures[0];
	}
	else
	{
		b->texture = standardBulletTextures[1];
	}

	b->health = FPS * 2;
	b->tick = standardTick;
	b->draw = standardDraw;
}

This function will create a bullet, set its `owner`, `damage`, vertical position (`y`), and velocity (`dx` and `dy`) using the parameters passed into the function. It will also assign the bullet's `texture` based on the side of the owner. In our game, cats will fire green bolts, while the enemies will fire red ones. Our textures reflect that. Finally, our bullets will live for 2 seconds. We're also assigning the `tick` and `draw` functions to standardTick and standardDraw.

We can now see how our tick and draw functions work. Starting with standardTick:


static void standardTick(Bullet *b)
{
	int x, y;

	if (b->owner == stage.player)
	{
		x = b->x - stage.camera.x;
		y = b->y - stage.camera.y;

		if (x <= -b->texture->rect.w || y <= -b->texture->rect.h || x >= SCREEN_WIDTH + b->texture->rect.w || y >= SCREEN_HEIGHT + b->texture->rect.h)
		{
			b->health = 0;
		}
	}
}

This function only applies to the player's bullets. What we're doing here is testing if the bullet has travelled off screen, and if so we're killing it by setting its `health` to 0. What this means is that the player cannot shoot or harm enemies or other things that aren't visible. This might sound odd, but it basically prevents the player from destroying objects that they might not want to, due to stray fire. It also pushes the player to engage the enemies, rather than spraying fire everywhere and hoping for the best. The behaviour was adopted in Project: Starfighter.

Next up we have standardDraw:


static void standardDraw(Bullet *b)
{
	int x, y;

	x = b->x - stage.camera.x;
	y = b->y - stage.camera.y;

	if (collision(x, y, b->texture->rect.w, b->texture->rect.h, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT))
	{
		blitAtlasImage(b->texture, x, y, 0, b->facing == FACING_LEFT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
	}
}

Nothing special here. Like our entities and effects, we're only rendering the bullet if it's on screen. The bullet's texture is flipped depending on the direction it is facing.

That's it for our bullets and firing. We need only update stage.c with the appropriate function calls and we're finished.

Starting with initStage:


void initStage(void)
{
	initStarfield();

	initBullets();

	initEntities();

	initEffects();

	initEntity("player");

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

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

We've added the call to initBullets. We next update doStage:


static void doStage(void)
{
	stage.engineEffectTimer -= app.deltaTime;

	doEntities();

	doBullets();

	doEffects();

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

	doCamera();

	doBackground(stage.ssx * 0.125 * app.deltaTime, stage.ssy * 0.125 * app.deltaTime);

	if (stage.engineEffectTimer <= 0)
	{
		stage.engineEffectTimer = 1;
	}
}

We've added a call to doBullets. Finally, we update `draw`:


static void draw(void)
{
	drawBackground(background);

	drawStarfield();

	drawEntities();

	drawBullets();

	drawEffects();
}

To draw our bullets, we just call drawBullets.

There we have it. In two parts we've added the ability to fly our ship around and fire our guns. What we need now is something to shoot. So, in the next part we're going to add in some enemies to destroy.

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