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 Honour of the Knights (First Edition) (The Battle for the Solar System)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and an Imperial nation's civil war than either the Confederation Stellar Navy or the government are willing to let on.

Click here to learn more and read an extract!

« Back to tutorial listing

— Mission-based 2D shoot 'em up —
Part 29: Mission: The Gravlax (Boss)

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

Introduction

Every good shooter has a boss fight, and our one will be against a Greeble frigate known as The Gravlax. This is a formiddiable opponent, that the player must be well prepared for. Our boss will be composed of three parts: the main body, a fin, and a wing. All three parts can be targetted and have their own health pools. Destroying the fin and wing will prevent them from being able to attack, while destroying the main body will be required to complete the mission. The misson itself has but one objective - to destroy The Gravlax.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-29 to run the code. You will see a window open displaying the intermission's planets screen. Select Purr, then enter the comms section to start the mission. Play the game as normal. Once The Gravlax is defeated, the mission will be effectively over. Once again, The Gravlax is a very powerful opponent, so it is recommended that the player edit game.c to increase their fighter's damage, weapons, output, and health to stand a better chance (unless one wishes to play all the other mission to legtitmately earn catnip for upgrades). Once you're finished, close the window to exit.

Inspecting the code

At this point in development, The Gravlax boss mission means that we'll be focusing almost exclusively on the compilation unit we've created to handle it. We'll touch on other parts of the code as the mission itself requires, but otherwise there will be very few updates outside of the boss's code.

You might be wondering what's with the name of the frigate, and why Leo makes the comment about it sounding delicious. A funny story about that - I decided on the name of the boss due to an advertising campaign run by the telecoms company Three, in the UK.

Gravlax is a fish dish. Cats like fish. Therefore, the frigate sounds very tasty indeed. So, now you know!

With that out of the way, how about we look at the code additions and updates. Firstly, we've updated structs.h:


typedef struct
{
	double      reload;
	double      health;
	int         maxHealth;
	double      thinkTime;
	double      hitTimer;
	SDL_Point   offset;
	AtlasImage *destroyedTexture;
	Entity     *owner;
} Component;

We've created a new struct here called Component. This will represent part of a Boss's body. In the case of the Gravlax, it will be the fin or the wing. Quite a number of fields here, but it looks lot like a fighter. `reload` is the rate of fire. `health` is its health. maxHealth is the maximum health. thinkTime is the think / action time. hitTimer is the hit timer. `offset` is a special one - these are coordinates that we'll use to determine where the component is, relative to the boss's main body. We'll see more on this later. destroyedTexture is the texture to use when the component is killed. `owner` is the boss itself.

Next up, we've got the Boss struct:


typedef struct
{
	double   dx, dy;
	double   reload;
	double   health;
	int      maxHealth;
	double   shield;
	int      maxShield;
	double   thinkTime;
	double   hitTimer;
	double   shieldHitTimer;
	Entity **components;
	int      numComponents;
} Boss;

Again, this looks quite a lot like Fighter, but with some extras. `dx` and `dy` are the velocity. `reload` is the fire rate. `health` and maxHealth are the health values, while `shield` and maxShield are the shield values. thinkTime, hitTimer, and shieldHitTimer are the action times, hit times, and shield hit times. Yep, just like a Fighter. What's new is the `components` variable. This is an array that we'll allocate, containing the boss's components. Finally, numComponents is the count of these components.

Okay, so we've got our boss data structures all sorted out. Now for the boss implementation itself. We've created gravlax.c to handle all of its logic and rendering functions. There are quite a number of functions here, some with more obvious goings on than others. We'll try not to linger too long on things that should be obvious by now, or else everyone's going to get bored!

Starting with initGravlax:


void initGravlax(Entity *e)
{
	Boss *b;

	b = malloc(sizeof(Boss));
	memset(b, 0, sizeof(Boss));
	b->health = b->maxHealth = 750;
	b->shield = b->maxShield = 150;

	e->side = SIDE_GREEBLE;
	e->facing = FACING_LEFT;
	e->data = b;
	e->texture = getAtlasImage("gfx/fighters/gravlax01.png", 1);

	e->tick = tick;
	e->draw = draw;
	e->takeDamage = takeDamage;
	e->die = die;
	e->destroy = destroy;

	createComponents(e, b);

	stage.missionTarget.health = &b->health;
	stage.missionTarget.maxHealth = b->maxHealth;

	shieldHitTexture = getAtlasImage("gfx/effects/shieldHit.png", 1);
}

Okay, a factory function. We're creating a Boss, and assigning it all the relevant details, such as the `health` and `shield`. For the entity (`e`) itself, we're assigning the `side`, `data` value, `texture`, and function pointers that we've seen before. We're then calling createComponents. This is where we'll be creating our fin and wing. We'll get to those in a bit. We're also setting Stage's missionTarget details to those of our boss, just like we did with the SS Goodboy, so we can track its status. Finally, we're loading our shieldHitTexture. Our fighters have this in a common shared function, whereas the Gravlax needs to handle all these itself.

Now for createComponents:


static void createComponents(Entity *owner, Boss *b)
{
	Component *c;
	Entity    *e;

	b->numComponents = 2;

	b->components = malloc(sizeof(Entity *) * b->numComponents);

	// === Fin ===
	c = malloc(sizeof(Component));
	memset(c, 0, sizeof(Component));
	c->owner = owner;
	c->health = c->maxHealth = 250;
	c->offset.y = -98;
	c->offset.x = 30;
	c->destroyedTexture = getAtlasImage("gfx/fighters/gravlax02Destroyed.png", 1);

	e = spawnEntity();
	e->side = SIDE_GREEBLE;
	e->data = c;
	e->texture = getAtlasImage("gfx/fighters/gravlax02.png", 1);
	e->tick = componentTick;
	e->draw = componentDraw;
	e->takeDamage = componentTakeDamage;
	e->die = die;
	e->destroy = destroyComponent;
	b->components[0] = e;

	// === Wing ===
	c = malloc(sizeof(Component));
	memset(c, 0, sizeof(Component));
	c->owner = owner;
	c->health = c->maxHealth = 350;
	c->offset.x = 80;
	c->offset.y = 78;
	c->destroyedTexture = getAtlasImage("gfx/fighters/gravlax03Destroyed.png", 1);

	e = spawnEntity();
	e->side = SIDE_GREEBLE;
	e->data = c;
	e->texture = getAtlasImage("gfx/fighters/gravlax03.png", 1);
	e->tick = componentTick;
	e->draw = componentDraw;
	e->takeDamage = componentTakeDamage;
	e->die = die;
	e->destroy = destroyComponent;
	b->components[1] = e;
}

This is where we're creating the fin and wing of the Gravlax. We're setting the Boss's (`b`'s) numComponents to 2, and then mallocing the Boss's `components` to an array of Entity pointers of the same size. Next, we make the components themselves. Each one will have an Entity created for them, with a Component created and added to the Entity's `data` field. Each component has its attributes set (including `health`, `offset`, destroyedTexture, etc). The component's `owner` is set as the Boss entity (`e`).

So, just a basic factory / setup function. Onwards to the main `tick` function now:


static void tick(Entity *self)
{
	Boss *b;

	b = (Boss *)self->data;

	b->thinkTime = MAX(b->thinkTime - app.deltaTime, 0);
	b->reload = MAX(b->reload - app.deltaTime, 0);
	b->hitTimer = MAX(b->hitTimer - app.deltaTime, 0);
	b->shieldHitTimer = MAX(b->shieldHitTimer - app.deltaTime, 0);

	if (b->shieldHitTimer == 0)
	{
		b->shield = MIN(b->shield + ((1.0 / FPS) * app.deltaTime), b->maxShield);
	}

	b->dx = MIN(MAX(b->dx, -MAX_SPEED), MAX_SPEED);
	b->dy = MIN(MAX(b->dy, -MAX_SPEED), MAX_SPEED);

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

	huntPlayer(self, b);

	updateComponents(self, b);
}

This is the `tick` call that is made by the main body. It looks just like the fighterTick function from fighters.c! However, it's doing two things different - it's calling a function named huntPlayer and another called updateComponents.

Let's look at huntPlayer first:


static void huntPlayer(Entity *self, Boss *b)
{
	int speed;

	if (!stage.player->dead)
	{
		if (b->thinkTime <= 0)
		{
			calcSlope(stage.player->x, stage.player->y, self->x, self->y, &b->dx, &b->dy);

			if (rand() % 12 > 0)
			{
				speed = 5 + rand() % 12;
			}
			else
			{
				speed = 20;
			}

			b->dx *= speed;
			b->dy *= speed;

			b->thinkTime = FPS + (FPS * rand() % 2);
		}

		if (stage.player->x < self->x)
		{
			self->facing = FACING_LEFT;
		}
		else
		{
			self->facing = FACING_RIGHT;
		}

		doMainAttack(self, b);
	}
}

This does as its name implies - it causes the boss to chase down the player. Essentially, this is the Boss's main AI. We first test that the player isn't dead, and then calculate the direction that we need to go to reach the player, via calcSlope. We'll randomly choose the speed we want to move at, and then update our thinkTime and `facing`. Notice how the boss chases down the player relentlessly; we're not going to have it sit around and just get shot..! Finally, we're calling doMainAttack.

The doMainAttack function is as follows:


static void doMainAttack(Entity *self, Boss *b)
{
	int distX, distY;

	if (b->reload == 0 && ((self->facing == FACING_LEFT && stage.player->x < self->x) || (self->facing == FACING_RIGHT && stage.player->x > self->x)))
	{
		distX = abs(self->x - stage.player->x);
		distY = abs(self->y - stage.player->y);

		if (distX < SCREEN_WIDTH * 0.65 && distY < 50)
		{
			if (rand() % 25 != 0)
			{
				addStraightBullet(self, 1, self->y + (self->texture->rect.h / 2) - 8, self->facing == FACING_RIGHT ? 24 : -24, 0);
				addStraightBullet(self, 1, self->y + (self->texture->rect.h / 2) + 8, self->facing == FACING_RIGHT ? 24 : -24, 0);

				b->reload = 8;
			}
			else
			{
				fireRedBeam(self);
				fireRedBeam(self);
				fireRedBeam(self);

				b->reload = FPS * 3;
			}
		}
	}
}

Once again, this looks very much like the attack function we have in ai.c - we need to test that we're able to fire (`reload` == 0) and that we're facing the right way, before choosing which weapon we wish to use. The boss's main body will either fire dual shots, or unleash the devestating red beam! We'll adjust the `reload` times depending on which weapon is fired. Notice how when the Gravlax fires the red beam, it does so three times, to layer three of the beams on top of one another, so that the damage is much greater than a single shot.

Next up we have updateComponents:


static void updateComponents(Entity *self, Boss *b)
{
	Component *c;
	Entity    *e;
	int        i;

	for (i = 0; i < b->numComponents; i++)
	{
		e = b->components[i];

		c = (Component *)e->data;

		e->facing = self->facing;

		if (e->facing == FACING_RIGHT)
		{
			e->x = self->x + c->offset.x;
		}
		else
		{
			e->x = (self->x + self->texture->rect.w) - (c->offset.x + e->texture->rect.w);
		}

		e->y = self->y + c->offset.y;
	}
}

This is simple function to understand. What we're doing here is looping through all our components, and positioning them around the Gravlax's main body (`e`), using their offsets. We're also taking the facing into account when positioning the components along the x axis, so that we can align them properly. Our components don't move on their own, so will never drift off anywhere. This function ensures they remain in place.

Now for componentTick:


static void componentTick(Entity *self)
{
	Component *c;

	c = (Component *)self->data;

	c->thinkTime = MAX(c->thinkTime - app.deltaTime, 0);
	c->reload = MAX(c->reload - app.deltaTime, 0);
	c->hitTimer = MAX(c->hitTimer - app.deltaTime, 0);

	if (c->health > 0)
	{
		doComponentAttack(self, c);
	}

	self->dead = c->owner->dead;
}

This function is used by our components. As with the main body, it updates thinkTime, `reload`, and hitTimer, but it doesn't actually have any AI movement itself. That is left up to the main body. It does have its own attack functions, however. We first check that the component is still alive (`health` > 0), and then call doComponentAttack. Finally, we want the component to die if the main body is destroyed. We do this simply by assigning the component's `dead` flag to be the same as its owner's `dead` flag. So, if we happen to destroy the Gravlax's main body before the component, this will ensure that the component doesn't remain hanging around, attacking us..!

Now for doComponentAttack:


static void doComponentAttack(Entity *self, Component *c)
{
	int distX, distY, dx;

	if (c->reload == 0 && ((self->facing == FACING_LEFT && stage.player->x < self->x) || (self->facing == FACING_RIGHT && stage.player->x > self->x)))
	{
		distX = abs(self->x - stage.player->x);
		distY = abs(self->y - stage.player->y);
		dx = self->facing == FACING_RIGHT ? 24 : -24;

		if (distX < SCREEN_WIDTH * 0.65 && distY < 75)
		{
			if (rand() % 25 > 0)
			{
				addStraightBullet(self, 1, self->y + (self->texture->rect.h / 2), dx, -2);
				addStraightBullet(self, 1, self->y + (self->texture->rect.h / 2), dx, 0);
				addStraightBullet(self, 1, self->y + (self->texture->rect.h / 2), dx, 2);

				c->reload = 10;
			}
			else
			{
				fireRocket(self, self->facing);

				c->reload = FPS;
			}
		}
	}
}

Yep, this looks as expected - we're testing if we're able to attack the player, and then we'll either fire a three way spread or launch a rocket. As with the Gravlax's main body, we'll be adjusting the `reload` times based on which weapon we fire.

That's all for our logic. In summary, our boss has its own AI and attacking routines, separate from the fighters. We want our boss to be aggressive, which is why it doesn't give up the chase on the player, and has some powerful weapons available. The player shouldn't be coming into this fight unprepared!

On to our rendering phase. Starting with `draw`:


static void draw(Entity *self)
{
	Boss *b;

	b = (Boss *)self->data;

	drawBossPart(self->x, self->y, self->texture, self->facing, b->hitTimer, b->shieldHitTimer);
}

This function is called by the boss's main body, and is delegating to a function called drawBossPart. We'll come to it in a moment. Notice that we're passing over the boss's hitTimer and shieldHitTimer values.

Next up, we have componentDraw:


static void componentDraw(Entity *self)
{
	Component *c;
	Boss      *b;

	c = (Component *)self->data;
	b = (Boss *)c->owner->data;

	drawBossPart(self->x, self->y, self->texture, self->facing, c->hitTimer, b->shieldHitTimer);
}

As the name suggests, this is used by our components. As with `draw`, componentDraw is delegating to drawBossPart. Notice here that we're passing over the component's hitTimer, but also the boss's shieldHitTimer! The components themselves don't have shields, but will use the boss's, as we'll see later on.

Now for drawBossPart:


static void drawBossPart(int x, int y, AtlasImage *texture, int facing, double hitTimer, double shieldHitTimer)
{
	double   shieldAlpha;
	SDL_Rect dest;

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

	if (hitTimer == 0)
	{
		blitAtlasImage(texture, x, y, 0, facing == FACING_LEFT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
	}
	else
	{
		SDL_SetTextureBlendMode(texture->texture, SDL_BLENDMODE_ADD);
		SDL_SetTextureColorMod(texture->texture, 255, 0, 0);
		blitAtlasImage(texture, x, y, 0, facing == FACING_LEFT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
		SDL_SetTextureBlendMode(texture->texture, SDL_BLENDMODE_BLEND);
		SDL_SetTextureColorMod(texture->texture, 255, 255, 255);
	}

	if (shieldHitTimer > 0)
	{
		dest.x = x - 12;
		dest.y = y - 12;
		dest.w = texture->rect.w + 24;
		dest.h = texture->rect.h + 24;

		shieldAlpha = MIN(255.0, 400.0 * (shieldHitTimer / SHIELD_HIT_TIME));

		SDL_SetTextureAlphaMod(shieldHitTexture->texture, shieldAlpha);
		SDL_RenderCopyEx(app.renderer, shieldHitTexture->texture, &shieldHitTexture->rect, &dest, 0, NULL, SDL_FLIP_NONE);
		SDL_SetTextureAlphaMod(shieldHitTexture->texture, 255);
	}
}

Very much like the routine for rendering a Fighter. We're adjusting the `x` and `y` according to the camera location, and then testing the value of hitTimer, to see if we want to tint the texture red or draw it normally. We then test the value of shieldHitTimer, and overlay the texture we're drawing with the shield texture. Again, much like how we do with our fighter rendering. We're doing this because the Gravlax's shield encompasses the entire ship, and not just the main body. The components can all be damaged individually, however, which is why they have separate hitTimers.

Our rendering is done! Time to consider the remaining functions. The Gravlax has its own takeDamage and componentTakeDamage functions, for handling the main body being damaged and a component being damaged. Let's look at takeDamage first:


static void takeDamage(Entity *self, double damage)
{
	Boss *b;
	int   x, y;

	b = (Boss *)self->data;

	if (b->shield > 0)
	{
		playSound(SND_SHIELD_HIT, CH_SHIELD_HIT);

		b->shield -= damage;

		if (b->shield < 0)
		{
			b->shield = -5;
		}

		b->shieldHitTimer = SHIELD_HIT_TIME;
	}
	else
	{
		playSound(SND_HIT, CH_HIT);

		b->health -= damage;

		b->hitTimer = FPS / 8;

		if (b->health <= 0 && !self->dead)
		{
			x = self->x + (self->texture->rect.w / 2);
			y = self->y + (self->texture->rect.h / 2);

			updateObjective("gravlax", 1);

			self->die(self);

			addExplosions(x, y, 100);

			addDebris(x, y, 200);

			playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
			playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
			playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
			playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
		}
	}
}

No real surprises here - the boss's shield will absorb the damage if it can. Otherwise, we'll apply the damage to the boss's health. hit timers are updated as we do this. When the boss's health is reduced to 0 or less, we'll call updateObjective, passing over "gravlax", and throw out a load of explosions and debris. No goodies to be had from defeating the boss, however. The reward here is victory itself!

componentTakeDamage comes next:


static void componentTakeDamage(Entity *self, double damage)
{
	Boss      *b;
	Component *c;
	int        x, y;

	c = (Component *)self->data;
	b = (Boss *)c->owner->data;

	if (c->health > 0)
	{
		playSound(SND_SHIELD_HIT, CH_SHIELD_HIT);

		if (b->shield > 0)
		{
			b->shield -= damage;

			if (b->shield < 0)
			{
				b->shield = -5;
			}

			b->shieldHitTimer = SHIELD_HIT_TIME;
		}
		else
		{
			playSound(SND_HIT, CH_HIT);

			c->health -= damage;

			c->hitTimer = FPS / 8;

			if (c->health <= 0)
			{
				self->side = SIDE_NONE;

				self->takeDamage = NULL;

				self->texture = c->destroyedTexture;

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

				addExplosions(x, y, 35);

				addDebris(x, y, 20);

				playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
				playSound(SND_EXPLODE_1 + rand() % 4, CH_ANY);
			}
		}
	}
}

Much like takeDamage, we'll damage the shield or the body. Notice that we're testing the boss's `shield`, not the component's (again, the component doesn't have a shield..!). When the component is destroyed, we're doing two special things. First, we're setting its `side` to SIDE_NONE, its takeDamage function to NULL, and also updating its `texture` to now use its destroyedTexture. Removing the function for taking damage means that our bullets will no longer strike it when we fire, and updating `side` to SIDE_NONE means that secondary weapons such as the homing missiles will no longer consider them to be targets. Firing homing missiles that zero in on dead targets would be annoying!

We have the `die` function next:


static void die(Entity *self)
{
	self->dead = 1;

	game.stats.enemiesDestroyed++;
}

We're just setting the `dead` flag to 1, and bumping our enemiesDestroyed stat counter. Yep, as big and powerful as the Gravlax may be, that still only counts as one.

Lastly, let's look at the `destroy` function. Here we'll see why we're using a pointer to a function for our entities:


static void destroy(Entity *self)
{
	Boss *b;

	b = (Boss *)self->data;

	free(b->components);

	free(self->data);
}

We're freeing the boss's `components` array (but not actually the entities), as well as its `data` itself. If we were to assume that an entity only has a `data` field set, the Gravlax boss would become a source of memory leaks in our game.

Finally, we have destroyComponent:


static void destroyComponent(Entity *self)
{
	free(self->data);
}

This function is free to just remove the data (a Component) we created.

That's it for gravlax.c! There was rather a lot of it, but as you can see, it was all the necessary functions packed into a single file. There is nothing that can be shared, unlike the fighters.

We have just a handful more things to add in, and this part is done. You'll notice that once we defeat the Gravlax, the remaining fighters run away from the player. This is handled by a script command called "RETREAT_ENEMIES". If we head over to script.c, we can see how this is being handled in doScript:


void doScript(void)
{
	// snipped

	for (s = head.next; s != NULL; s = s->next)
	{
		if (s->running)
		{
			stage.scriptRunning = 1;

			s->delay = MAX(s->delay - app.deltaTime, 0);

			if (s->delay == 0)
			{
				line = s->lines[s->lineNum];

				sscanf(line, "%s", command);

				// snipped

				else if (strcmp(command, "RETREAT_ENEMIES") == 0)
				{
					retreatEnemies();
				}
				else
				{
					SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL, "ERROR: Unrecognised script command '%s'\n", command);
					exit(1);
				}

				s->lineNum++;

				s->running = s->lineNum < s->numLines;
			}
		}
	}
}

We've added a check for a command named "RETREAT_ENEMIES" and calling retreatEnemies when we encounter it. The retreatEnemies function is found in ai.c:


void retreatEnemies(void)
{
	Entity *e;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		if (e->side == SIDE_GREEBLE)
		{
			((Fighter *)e->data)->ai.type = AI_EVADE;
		}
	}

	stage.totalEnemies = 0;
}

A very simple function - we're looping through all the entities in the stage, looking for any whose `side` is SIDE_GREEBLE. Once found, we're setting the Fighter's ai `type` to AI_EVADE. We're also setting totalEnemies to 0, so that no more enemies spawn in the stage, in case the player decides to start killing off those who are running away. A word of caution here - we're assuming that the entities are Fighters. This works in our game, since we know we certain they will be at the point we're calling this function. If this game were to be expanded, we'd want to give our entities a type, and test against that, along with the `side`. So, for example, we could set the `type` to ET_FIGHTER (1), and assign this to all our fighters. This will protect us from a bad cast, which could well occur in the logic above.

Before we finish, let's take a look at our final entity list, in entityFactory.c:


void initEntityFactory(void)
{
	memset(&head, 0, sizeof(InitFunc));
	tail = &head;

	addInitFunc("player", initPlayer);
	addInitFunc("greebleLightFighter", initGreebleLightFighter);
	addInitFunc("greebleDualFighter", initGreebleDualFighter);
	addInitFunc("greebleBomber", initGreebleBomber);
	addInitFunc("greeblePOWShip", initGreeblePOWShip);
	addInitFunc("ssGoodboy", initSSGoodboy);
	addInitFunc("gravlax", initGravlax);
}

All our entities are registered with our factory, so that we can spawn these types during our mission data files.

And that's a wrap for our main game! We've only got a handful of things left to do, and we'll be completely finished. Our next step is to introduce a title screen, and then add in some finishing touches. Making a game such as this is no small task, but hopefully you can see it's not quite as difficult as you might've first expected. Once again, the ground work is there to expand the game out a lot. We could have new star systems that the player moves to, and our script allows us to make some varied and interesting missions.

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