« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 8: Secondary weapon: Homing missiles

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

Introduction

We've implmented our first secondary weapon - a rocket. It's powerful, but flies straight ahead. What would be interesting is to have a fire-and-forget homing missile. In this part, we're going to add just that. It won't be as powerful as the rocket, but being able to loose it, knowing it will never fail to strike a target, makes up for that.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-08 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 (homing missiles). Play the game as normal. Firing a missile consumes ammo. Notice how the missiles select and hunt down their targets until they are destroyed. Once you're finished, close the window to exit.

Inspecting the code

Just like with adding in our rockets, adding in our homing missiles is a very simple task. We have had to made a few extra modifications, however, since missiles need to gather information about their targets.

So, starting with defs.h:


enum
{
	SW_NONE,
	SW_ROCKET,
	SW_HOMING_MISSILE
};

We've added an enum called SW_HOMING_MISSILE. This will represent our homing missile secondary weapon.

Next, we've modified the Bullet struct in structs.h:


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

To this struct, we've added two new variables: `angle` and `target`. `angle` will be the angle of the bullet, while `target` will be its target. Our missiles rotate as they turn, thus the need for an angle.

Next, we can move on to bullets.c, where we've added a few new functions. Starting with fireHomingMissile:


void fireHomingMissile(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 = 25;
	b->health = FPS * 30;

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

	b->tick = homingMissileTick;
	b->draw = homingMissileDraw;
}

Just like fireRocket, this function creates a bullet to represent our missile. We're setting its various attributes (including the initial `angle` and speed (`dx`), depending on which way the owner is facing), and then setting its `tick` and `draw` functions to homingMissileTick and homingMissileDraw, respectively. Notice that our homing missiles last for a long time - they have a `health` of 30 seconds, meaning they'll be around for a good while before they expire.

Easy enough. Now onto homingMissileTick. This is where things get a little more complicated:


static void homingMissileTick(Bullet *b)
{
	int    wantedAngle, dir, diff, distance, turn;
	double thrust, v, speed;

	if (stage.rocketEffectTimer <= 0)
	{
		addRocketEngineEffect(b->x - sin(TO_RAIDANS(b->angle)) * 15, b->y - -cos(TO_RAIDANS(b->angle)) * 10);
	}

	if (b->target == NULL || b->target->dead)
	{
		findNewTarget(b);
	}

	if (b->target != NULL)
	{
		speed = 4;

		wantedAngle = (int)getAngle(b->x, b->y, b->target->x, b->target->y);

		diff = abs(wantedAngle - b->angle);

		if (diff > 3)
		{
			turn = 3;

			distance = getDistance(b->x, b->y, b->target->x, b->target->y);

			// tighter turning circle when closer
			if (distance <= 500)
			{
				turn += ceil((1.0 * distance) / 100);
			}

			dir = (wantedAngle - (int)b->angle + 360) % 360 > 180 ? -turn : turn;

			b->angle += dir * app.deltaTime;

			b->angle = fmod(fmod(b->angle, 360) + 360, 360);

			// don't accelerate so hard when closer (prevent overshooting goal)
			speed = 1;
		}

		b->dx += sin(TO_RAIDANS(b->angle)) * speed * app.deltaTime;
		b->dy += -cos(TO_RAIDANS(b->angle)) * speed * app.deltaTime;

		thrust = sqrt((b->dx * b->dx) + (b->dy * b->dy));

		if (thrust > MAX_HOMING_SPEED)
		{
			v = (MAX_HOMING_SPEED / sqrt(thrust));
			b->dx = v * b->dx;
			b->dy = v * b->dy;
		}
	}

	checkCollisions(b);
}

Okay, there looks like there's quite a bit going on here, but it's not as bad as it might first appear. Let's break it down one bit at a time. First, we're checking rocketEffectTimer, to see if we want to produce an engine trail, just like for our rocket. When it comes to positioning the engine trail, we're performing a calculation using sin and cos, along with the missile's current angle, to place the effect at the rear of the missile. Our rocket texture is 38 x 18 pixels, so using 15 and 10 with the calculations more or less places the engine effect at the back of the object, no matter which way its rotated.

Next, we check if the missile doesn't have a target (`target` is NULL) or if it's current target is dead, and if so we call findNewTarget. We'll see this function in a bit (although, one can likely already guess how it works!).

We then test to see if the missile has a target before moving onto the next phase. This is the part where our missile will chase after its target. We first set a variable called `speed` to 4. This is the default velocity at which our missile will move. Next, we determine the angle between the missile and its target, and assign this to a variable called wantedAngle. We turn subtract the bullet's `angle` from wantedAngle, and grab the absolute value (we don't want it to be negative), assigning the result to `diff`. We then test if `diff` is greater than 3 and proceed with the next part. What we wish to achieve here is for our missile to turn and face its target, but only if the target is outside of a certain angle threshold. We do this to prevent the missile from constantly trying to perfect its angle (almost impossible), which would result in it wobbling a lot.

So, knowing that `diff` is more than 3, we're going to making some adjustments to our direction. We first set a variable called `turn` to 3. This will control how quickly we want to now rotate to face the angle we want. Next, we test how far away we are from our target, with a call to getDistance. If the distance is 500 or less, we're going to increase the value of `turn` by 1 point per 100 units of distance (rounded up). So, the further we are away, the harder we'll turn. With our turn speed determined, we'll decide which way we wish to turn, either clockwise or anti-clockwise. We do this by determining if the difference between the wanted and current angle is more than 180, and assigning `dir` to be either the negative or positive value of `turn`. Finally, we add `dir` to our missile's `angle`, ensuring it remains within 360 degrees. Next, we reduce `speed` down to 1, so that our missile doesn't overshoot its target, and will make sharper turns.

With our turning now done, we want to apply our missile's thrust. We can once again do this will the help of cos and sin, along with our missile's `angle`, and the speed value we previously determined. The results will be added to `dx` and `dy`, to make the missile accelerate. Finally, we want to ensure that our missile doesn't fly too fast. We do this by working out how fast it's going, and applying that value to `thrust` (with the help of square root). We then check if `thrust` is more than MAX_HOMING_SPEED and slow ourselves down if so, once more with the help of square root. I'm not going to pretend to understand the maths behind the physics for this step; it's something that can be found in a number of textbooks!

The very last thing we do is call checkCollisions, to see if our homing missile has hit any targets.

Complicated, yes. Deadly and efficient? Also yes.

Let's move onto findNewTarget:


static void findNewTarget(Bullet *b)
{
	Entity *e;
	long    distance, bestDistance;

	b->target = NULL;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		if (e->side != SIDE_NONE && e->side != b->owner->side && e->takeDamage != NULL)
		{
			distance = getDistance(b->x, b->y, e->x, e->y);

			if (b->target == NULL || distance < bestDistance)
			{
				b->target = e;

				bestDistance = distance;
			}
		}
	}
}

The code above is very much like that for an AI seeking something to attack. We first NULL our missile's `target`. Next, we scan all entities, searching for one that is not on the same side as the missile's owner, and can be hurt (takeDamage is set). The nearest one will become the missile's new `target`. If none are found, our missile's `target` will remain NULL, and it will remain on its current path.

The last function in bullets.c we need to consider is homingMissileDraw:


static void homingMissileDraw(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))
	{
		blitRotated(b->texture, x, y, b->angle - 90);
	}
}

Like the standard bullet drawing function, we're testing if the homing missile is visible on screen before rendering it. As our homing missile can rotate, we're rendering it with a call to blitRotated.

That's our changes to bullets.c done. We need only make a couple of other updates in order to use the new weapon. Starting first with player.c, we've updated fireSecondary:


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;

			case SW_HOMING_MISSILE:
				fireHomingMissile(self, self->facing);
				break;

			default:
				break;
		}

		game.kite.ammo--;
	}
}

We've added a check for SW_HOMING_MISSILE to our switch statement, and are calling the fireHomingMissile function in response.

Last but not least, we're going to update initGame, to give ourselves the weapon:


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_HOMING_MISSILE;
}

We've set Game's kite's secondaryWeapon to SW_HOMING_MISSILE.

There we go, the ability to fire homing missiles. The logic may look a little complex in places, but we can't fault the results. Thankfully, this is the most difficult weapon to understand, with the remaining ones very simple to implement.

And what could be easier to understand than a rectangle? Since beam weapons are a staple of shoot 'em ups (often seen in the hands of bosses), in the next part we're going to grant the player the chance to use one themselves.

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