« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 7: Secondary weapon: Rockets

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

Introduction

Before we move onto implementing missions and objectives, we should take the opportunity to work on our secondary weapons. We will have 4 weapons and a shield system in the end, all which will have unique properites. We're going to start with a simple one: rockets.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-07 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. Press I to fire your secondary weapon (rockets). Play the game as normal. Firing a rocket will consume ammo. Collect more ammo power-ups from defeated enemies to replenish your stock. Once you're finished, close the window to exit.

Inspecting the code

Adding in our rockets is very, very easy. Rockets are basically bullets, with high damage, and a different `tick` function to that which we already have.

We'll start with the update to defs.h:


enum
{
	SW_NONE,
	SW_ROCKET
};

We've created a new enum to represent our secondary weapon (SW being short for Secondary Weapon). SW_NONE will mean we don't have a secondary weapon, while SW_ROCKET will declare we have rockets.

Now on to structs.h:


typedef struct
{
	int catnip;
	struct
	{
		int health, maxHealth;
		int ammo;
		int damage;
		int reload;
		int output;
		int secondaryWeapon;
	} kite;
} Game;

We've updated Game's `kite`'s struct, adding in secondaryWeapon. This will be used to determine the type of secondary weapon we're using. Again, note that we're putting this into our Game struct, rather than assigning it to the player's fighter directly. This is so that the player can choose which weapon to use between missions.

Finally, we've updated the Stage struct:


typedef struct
{
	// snipped
	double      engineEffectTimer, rocketEffectTimer;
	int         numActiveEnemies;
	Entity     *player;
	PointF      camera;
} Stage;

We've added in rocketEffectTimer. Like engineEffectTimer, this will be used to control how often our rockets stream flame trails.

With that done, we can move over to bullets.c, where we'll implement our new rocket weapon. First of all, we're updating 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);

	rocketTexture = getAtlasImage("gfx/bullets/rocket.png", 1);
}

The only new thing we're doing here is grabbing a texture for our rocket sprite, and assigning it to rocketTexture.

Next up, we've added a new function called fireRocket:


void fireRocket(Entity *owner, int facing)
{
	Bullet *b;

	b = spawnBullet(owner);
	b->y = owner->y + (owner->texture->rect.h / 2);
	b->texture = rocketTexture;
	b->facing = facing;
	b->damage = 50;
	b->health = FPS * 2;

	if (facing == FACING_LEFT)
	{
		b->x = owner->x;
		b->dx = -ROCKET_SPEED;
	}
	else
	{
		b->x = owner->x + owner->texture->rect.w;
		b->dx = ROCKET_SPEED;
	}

	b->tick = rocketTick;
	b->draw = standardDraw;
}

A very simple function. We're basically creating a bullet, and giving it the rocketTexture texture. We're setting its `damage` to 50 and making it live for 2 seconds (via its `health`). We're also testing to see which direction the owner of the rocket is facing (in this case, the player), and setting the rocket's starting `x` position and `dx` (it's speed) accordingly; we want to make sure the rocket goes in the right direction and originates from the front of the firing craft. Finally, we're setting the bullet's `tick` to a new function called rocketTick, and its `draw` function to standardDraw.

So far so good. Now to look at the rocketTick function:


static void rocketTick(Bullet *b)
{
	if (stage.rocketEffectTimer <= 0)
	{
		addRocketEngineEffect(b->x + (b->facing == FACING_LEFT ? b->texture->rect.w : 0), b->y + b->texture->rect.h - 10);
	}

	checkCollisions(b);
}

You might remember that the player's bullets are killed off if they leave the screen. Well, we're not doing that test in this function; our rockets can fly until they expire naturally. So, this function merely tests if Stage's rocketEffectTimer is 0 or less, and then creates an engine effect at the rear of the rocket (by calling addRocketEngineEffect, found in effects.c). We're also calling checkCollisions, to see if the rocket has struck an enemy.

That's it for bullets.c! Very simple. Now we can look at the other compilation units that we've updated.

If we look at game.c, we can see we've updated initGame:


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

	game.kite.health = game.kite.maxHealth = 10;
	game.kite.reload = MIN_KITE_RELOAD;
	game.kite.output = 1;
	game.kite.damage = 1;
	game.kite.ammo = MAX_KITE_AMMO;
	game.kite.secondaryWeapon = SW_ROCKET;
}

We're setting Game's kit's secondaryWeapon to SW_ROCKET, to tell our game we have rockets as our secondary weapon. This is just for testing purposes. In our full game, the player won't have a starting secondary weapon, and will need to buy one.

Now onto player.c, where we've made one update and one addition, starting with doPlayerControls:


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

	if (isControl(CONTROL_SECONDARY))
	{
		clearControl(CONTROL_SECONDARY);

		fireSecondary(self);
	}

	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 testing if we've pressed the control for firing our secondary weapon (isControl is found in controls.c). If so, we're going clear the control, and then call a new function named fireSecondary. Unlike when we fire our main guns, we want the player to press the button, rather than hold it down. Our secondary weapons don't have a reload or firing detaly, so this will ensure we don't use up all our ammo in one go, which would be annoying!

The fireSecondary function is defined so:


static void fireSecondary(Entity *self)
{
	if (game.kite.ammo > 0 && game.kite.secondaryWeapon != SW_NONE)
	{
		switch (game.kite.secondaryWeapon)
		{
			case SW_ROCKET:
				fireRocket(self, self->facing);
				break;

			default:
				break;
		}

		game.kite.ammo--;
	}
}

Another simple function. We're first testing if we have any `ammo` (and also a valid secondary weapon), and then testing which kind of weapon we have. We only have SW_ROCKET right now, so we're calling the fireRocket function. With that done, we're decrementing our `ammo`, since we've just used some.

Finally, let's look at stage.c, where we've updated doStage:


static void doStage(void)
{
	stage.numActiveEnemies = 0;

	stage.engineEffectTimer -= app.deltaTime;
	stage.rocketEffectTimer -= app.deltaTime;

	// snipped

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

	if (stage.rocketEffectTimer <= 0)
	{
		stage.rocketEffectTimer = 0.15;
	}
}

Just like our engineEffectTimer, we're decreasing rocketEffectTimer, and setting it to 0.15 if it falls to 0 or less.

And that's it! We can now fire some powerful rockets as a secondary weapon, so long as we have the ammo to do so. If not, we can just shoot down some enemies, and hope they release the powerups we need (or, later, buy ammo from the Mouse Bros. shop - but that's for another time).

Rockets are a good weapon, and do a lot of damage. What would be better is if we could launch one that will hunt down our enemies, letting us "fire and forget". So, in the next part we'll look into adding in homing missiles!

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