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 Battle for the Solar System (Complete)

The Pandoran war machine ravaged the galaxy, driving the human race to the brink of destruction. Seven men and women stood in its way. This is their story.

Click here to learn more and read an extract!

« 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:

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