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
SDL 1 tutorials (outdated)

Latest Updates

SDL2 turn-based strategy tutorial
Thu, 14th April 2022

Water Closet ported to PlayStation Vita
Tue, 4th January 2022

The Legend of Edgar 1.35
Sat, 1st January 2022

Achievements tutorial
Thu, 2nd December 2021

SDL2 Rogue tutorial
Thu, 30th September 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (7)
games (39)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (10)
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

— A simple turn-based strategy game —
Part 20: Green Ghosts

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

Introduction

The final ghost that we're going to introduce is an exploding type. Upon spotting an enemy, it will walk right up to them, and stand there. Killing one of these exploding ghosts while they are next to you is a very bad idea, as they will detonate and cause damage to the surrounding as they go down.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS20 to run the code. You will see a window open like the one above, showing three wizards in a maze-like map, as well as a number of ghosts. Play the game as normal. Take out the Green Ghosts from a distance, as much as possible. Note how they cause a huge amount of damage to those standing within one square when they die. Once you're finished, close the window to exit.

Inspecting the code

Our Green Ghosts actually require a little more logic compared to their peers, but they are still not too difficult to implement; we just need to update a number of files and take some extra considerations into account.

Starting with defs.h:


enum {
	AI_PASSIVE,
	AI_COWARD,
	AI_NORMAL,
	AI_SLIMER,
	AI_EXPLODER
};

We've added in an enum value called AI_EXPLODER. This will be for the ghost AI to say it is an exploder type.

The exploders have no weapons, so we can go straight over to ai.c, where we've updated doAI:


void doAI(void)
{
	Unit *u;

	if (stage.currentEntity != NULL)
	{
		u = (Unit*) stage.currentEntity->data;

		if (u->ap != 0)
		{
			switch (u->ai.type)
			{
				// snipped

				case AI_EXPLODER:
					doExploder(u);
					break;

				default:
					u->ap = 0;
					break;
			}
		}
		else
		{
			nextUnit();
		}
	}
	else
	{
		endTurn();
	}
}

We've added in AI_EXPLODER to our ai `type` switch statement. We'll be calling a new function named doExploder, and passing in the current ghost's Unit data.

doExploder itself is quite a simple function:


static void doExploder(Unit *u)
{
	Entity *e;
	int distance, closest;

	stage.targetEntity = NULL;

	closest = 0;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_MAGE && hasLOS(stage.currentEntity->x, stage.currentEntity->y, e->x, e->y))
		{
			distance = getDistance(stage.currentEntity->x, stage.currentEntity->y, e->x, e->y);

			if (stage.targetEntity == NULL || distance < closest)
			{
				closest = distance;

				stage.targetEntity = e;
			}
		}
	}

	if (stage.targetEntity != NULL)
	{
		if (closest > 1)
		{
			moveToTarget(u);
		}
		else
		{
			u->ap = 0;
		}
	}
	else
	{
		moveRandom();
	}
}

The idea behind this function is that the Exploder will search for nearby, visible mages, and then move to the closest one.

We start by clearing Stage's targetEntity and setting a variable called `closest` to 0. Next, we loop through all the entities in the stage, looking for mages (ET_MAGE) that are the ghost's line of sight (hasLos). When we find one, we'll calculate the distance (as `distance`) and then test whether Stage's targetEntity is NULL or the distance of this mage is nearer than `closest`. We'll then set `closest` to the value of `distance` and set Stage's targetEntity as this mage.

With that done, we'll test if we found a target, and then check the value of `distance`. If it's greater than 1, we'll call a function named moveToTarget, otherwise we'll set the ghost's `ap` to 0. This means that if the ghost is standing more than 1 square away from its target, it will move closer to them. Otherwise, it will stand where it is..! Finally, if we didn't find a target after all these checks, we'll call moveRandom().

Easy enough, eh? moveToTarget is just as easy:


static void moveToTarget(Unit *u)
{
	int x, y, attempts;

	attempts = 25;

	u->ai.goal.x = stage.targetEntity->x;
	u->ai.goal.y = stage.targetEntity->y;

	do
	{
		x = u->ai.goal.x + rand() % 2 - rand() % 2;
		y = u->ai.goal.y + rand() % 2 - rand() % 2;

		x = MIN(MAX(x, 0), MAP_WIDTH - 1);
		y = MIN(MAX(y, 0), MAP_HEIGHT - 1);

		if (!isWall(x, y) && getEntityAt(x, y) == NULL)
		{
			createAStarRoute(stage.currentEntity, x, y);
		}

		attempts--;
	}
	while (stage.routeHead.next == NULL && attempts > 0);

	u->ai.goal.x = x;
	u->ai.goal.y = y;

	if (stage.routeHead.next == NULL)
	{
		u->ap = 0;
	}
}

We've seen this same logic used a couple of times before, so we'll just briefly go over it. The idea of this function is to have the ghost move to within one square of its target (Stage's targetEntity). We're taking the target's `x` and `y` and randomly adding -1 and 1, checking if the square we want to move to is free. We'll then calculate a route to the destination. If we're unable to move to the goal after several attempts, we'll simply set the Unit's `ap` to 0, so it stays where it is.

That's all we need to do for the actual AI of the ghost. As usual, we'll update addAIUnits, to make use of them:


static void addAIUnits(void)
{
	Entity *e;
	Unit *u;
	int i, x, y, ok;

	for (i = 0 ; i < 3 ; i++)
	{
		e = initEntity("Green Ghost");

		// snipped
	}
}

We're calling initEntity with "Green Ghost", to create our three Green Ghosts for this part.

Onto ghosts.c now, and here's where things get a bit more interesting than the previous ghosts. Starting with initGreenGhost:


void initGreenGhost(Entity *e)
{
	Unit *u;

	STRCPY(e->name, "Green Ghost");

	u = initGhost(e, "gfx/units/greenGhost.png");
	u->hp = u->maxHP = 5;
	u->ap = u->maxAP = 2;
	u->moveRange = 5;

	u->ai.type = AI_EXPLODER;

	e->die = explode;
}

We're setting the ghost's attributes as normal (notice how it has lower stats than others, include a much shorter moveRange). It's AI `type` is of course AI_EXPLODER, and it has no weapon. However, it's `die` function is new. When the Green Ghosts is killed, it will call a function named `explode`.

Let's take a look at that function now:


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

	if (!self->dead)
	{
		die(self);

		for (x = -1 ; x <= 1 ; x++)
		{
			for (y = -1 ; y <= 1 ; y++)
			{
				e = getEntityAt(self->x + x, self->y + y);

				if (e != NULL && (e->type == ET_MAGE || e->type == ET_GHOST))
				{
					e->takeDamage(e, 15);

					addHitEffect(MAP_TO_SCREEN(e->x), MAP_TO_SCREEN(e->y), 0, 255, 0);
				}
				else if (isGround(self->x + x, self->y + y))
				{
					stage.map[self->x + x][self->y + y].tile = TILE_SLIME;
				}
			}
		}
	}
}

This is the function that is responsible for making the exploder actually explode upon death. The first thing it does is check that its `dead` flag is not already set (to avoid two exploders endlessly killing each other). Next, it calls the regular `die` function. With that done, we'll entering into two for-loops, both from -1 to 1, to test the surroundings in a one square radius. For each iteration of the loop, we're calling getEntityAt for the position and testing the entity returned. If it's a mage or ghost, we're calling its takeDamage function, to inflict 15 points of damage! We're also calling addHitEffect, to generate some visual feedback. If there isn't a mage or ghost there, and the map square is a ground tile, we'll convert into a slime tile.

So, in short, an exploding ghost will inflict 15 points of damage to all those within one square of it, and also splatter the area with slime..! Nasty. We could, of course, have made it nastier, by inflicting damage based on distance, but that would make the game quite a bit tougher and perhaps make the exploders too difficult to deal with.

Before we press on, we should take a look at a small change we've made to the takeDamage function in units.c:


static void takeDamage(Entity *self, int damage)
{
	Unit *u;
	int r, g, b;

	if (!self->dead)
	{
		u = (Unit*) self->data;

		u->hp -= damage;

		// snipped
	}
}

We're now testing that the unit is not already dead before running the rest of the logic. This is to prevent situations where an exploder dies and kills another exploder, that dies, and inflicits damage on the first exploder again, etc. While this doesn't cause other game to lock up, it does produce lots of damage text, and also fills the hud with repeat messages. This little tweak prevents that issue from occurring.

You may well have realised something - it's possible for the ghosts to kill the mages as they die. So, what happens if the last ghost kills the last mage? What's our game going to do? Well, turning to stage.c, we've made a small update to handle that:


void endTurn(void)
{
	int attempts;

	attempts = 2;

	do
	{
		stage.turn = !stage.turn;

		stage.showRange = SHOW_RANGE_NONE;

		resetUnits();

		attempts--;
	}
	while (stage.currentEntity == NULL && attempts > 0);

	if (stage.currentEntity == NULL)
	{
		exit(0);
	}
}

For now, when endTurn is invoked, we'll attempt to swap turns just 2 times, before giving up and exiting the program. A bit heavy handed, yes, but as of right now, we don't have a proper win or lose state. So, this will do for the time being.

There we go! All our ghosts are implemented, as well as map generation, weapons, items, etc. Our game is very nearly finished! There are just a handful of things that need to be fixed up, and we also need to add in the aforementioned win / lose state. We'll be looking into that next.

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:

Directly

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal, and then download the tutorials directly from the main tutorials page.

SDL2_Tutorials.tar.gz 56.76MB 23rd April 2022

Click here to see the list of files in the archive

Mobile site