« Back to tutorial listing

— A simple turn-based strategy game —
Part 18: Blue Ghost

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

Introduction

The Blue Ghosts we're now going to add will exhibit a cautious behaviour. Upon spotting a mage, they will choose to either attack or retreat. So, fiercely in the middle when it comes to their decision making.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS18 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. Notice how the Blue Ghosts will sometimes attack and sometimes retreat from the mages. Once you're finished, close the window to exit.

Inspecting the code

Adding in our Blue Ghosts is even easier than adding in the Lilac Ghosts. We need to only update two files: ai.c and ghosts.c.

Let's start with ai.c. 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)
			{
				case AI_PASSIVE:
					doPassive();
					break;

				case AI_COWARD:
					doCoward();
					break;

				case AI_NORMAL:
					doNormal(u);
					break;

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

First of all, our Blue Ghosts will be using the AI_NORMAL profile (that we defined much earlier in this tutorial). We've modified the doNormal function a little, since it now takes a Unit as a parameter. We're passing across the current ghost's Unit into the function.

The doNormal function itself has also been tweaked:


static void doNormal(Unit *u)
{
	int doFallback, doAttack;

	lookForEnemies(u, &doFallback, &doAttack);

	if (doFallback || doAttack)
	{
		if (doFallback && doAttack && rand() % 2 == 0)
		{
			fireBullet();
		}
		else if (doAttack && rand() % 3 > 0)
		{
			fireBullet();
		}
		else
		{
			fallback();
		}
	}
	else
	{
		moveRandom();
	}
}

We're setting up two variables called doFallback and doAttack. These, we're passing into our lookForEnemies function (we'll see what changes we've made to this in a moment). These two variables will act as flags to say whether the ghost should retreat from or attack their targets. We first test whether doFallback or doAttack is set, and then take some random actions.

If both the doFallback and doAttack flags are set, we'll randomly decide (50-50 chance) whether we want to attack (via fireBullet). Otherwise, if the doAttack flag is set, there is a 2 in 3 chance that we'll also call fireBullet. If, however, we've decided not to attack, we'll call fallback.

Finally, if neither of these flags are set (there are no enemies visible), we'll call moveRandom, to make our ghost wander the stage.

Next, let's look at the changes we've made to lookForEnemies:


static void lookForEnemies(Unit *u, int *doFallback, int *doAttack)
{
	Entity *e;
	int distance, closest;

	stage.targetEntity = NULL;

	*doFallback = *doAttack = 0;

	closest = u->weapon.range + 1;

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

			if (distance < closest && hasLOS(stage.currentEntity->x, stage.currentEntity->y, e->x, e->y))
			{
				closest = distance;

				stage.targetEntity = e;
			}
		}
	}

	if (stage.targetEntity != NULL)
	{
		*doFallback = closest <= 5 && rand() % 5 > 3;

		*doAttack = closest <= 10 && stage.map[stage.targetEntity->x][stage.targetEntity->y].inAttackRange;
	}
}

As previously stated, we're now passing in the current ghost's Unit data; this is really just because the function needs it, and we had previously extracted it at the top of our doAI loop, so no need to extract it again. We're also passing in doFallback and doAttack, as references.

We're setting Stage's targetEntity to NULL, and also setting doFallback and doAttack to 0. Our code to look for an enemy to attack remains unchanged from when we first set it up (at the time, using the White Ghost), but after looking for a target, we're then evaluating them.

If Stage's targetEntity is not NULL (we found an enemy to attack), we're going to find out how we should respond. First, we'll test whether we want to set the doFallback flag. We'll check to see how close our enemy is. If the value of closest is 5 or less (meaning the enemy is within 5 squares of the ghost), and a random of 5 is greater than 3, we'll set the doFallback flag. For the doAttack flag, we'll test to see if the enemy is within 10 squares and is also in our attack range (the MapTile's inAttackRange is set).

That's it! That's all we need to do to have our Blue Ghosts decide to either attack or flee from a nearby enemy! We're simply evaluating the enemy we've chosen to attack and making a random decision based on what we see. There are things that we could add here, such as making them more likely to flee if there is more than 1 enemy nearby or also make them more likely to attack if they have a buddy nearby (who is in their line of sight). But right now, we've got a ghost that will keep its distance from the mages and attack at range, which makes them fun to battle.

The last thing we need to do in ai.c is update addAIUnits:


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

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

		e->side = SIDE_AI;

		do
		{
			x = rand() % MAP_WIDTH;
			y = rand() % MAP_HEIGHT;

			ok = isGround(x, y) && getEntityAt(x, y) == NULL && !isNearPlayer(x, y, 12);
		}
		while (!ok);

		e->x = x;
		e->y = y;

		u = (Unit*) e->data;

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

We want to create Blue Ghosts for this part, and so we're passing through "Blue Ghost" to the initEntity function (again, we've added the relevant data to entityFactory.c).

Lastly, let's define the ghost itself. If we move across to ghosts.c, we've added in a new function called initBlueGhost:


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

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

	u = initGhost(e, "gfx/units/blueGhost.png");
	u->hp = u->maxHP = 10;
	u->ap = u->maxAP = 2;
	u->moveRange = 11;
	u->weapon = getWeapon(WT_SLIME_BALL);

	u->ai.type = AI_NORMAL;
}

We're just setting the ghost's `name`, `texture`, and other attributes. Also, we're giving him a weapon, the slime ball that we were testing with in earlier parts.

Done! That was simple, eh? Things are getting easier and easier. How about we add a more interesting ghost next? Well, our Red Ghost will have some very unique properties, as it will have a different kind of weapon, one that can create slime pools. It will be capable of not only attacking the player, but also targetting the map around them, to make life a little more difficult for our intrepid trio of wizards.

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