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 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 4: AP (action point) handling

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

Introduction

Traditionally, in turn-based games units have a set amount of action points (or even time units, if you wish to get even more fine-grained) to work with during their turn. In this part, we're going to look at adding in action points for our mages. They will be able to take 2 actions each per turn, as displayed on the HUD.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS04 to run the code. You will see a window open like the one above, showing three wizards in a small room, surrounded by walls. Move the mages around as normal. Notice how, on the HUD, the AP decreases each time a mage takes a move. Once the AP reaches zero, they can no longer move (also note how the range display shows no available squares to move into). Press Space to end your turn and restore all the mages' AP. Once you're finished, close the window to exit.

Inspecting the code

Adding in AP support to limit our movement isn't difficult, at all. We only need to think about how and where it is used. We'll start with looking at the updates to structs.h.


typedef struct {
	int ap, maxAP;
	int moveRange;
} Unit;

We've updated the Unit struct, to add in two new fields: `ap` and maxAP, representing the action points and maximum action points of a unit, respectively.

Next, let's turn to units.c, where we've updated the `move` function:


static void move(void)
{
	Node *n;

	moveTimer -= app.deltaTime;

	if (moveTimer <= 0)
	{
		n = stage.routeHead.next;

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

		stage.currentEntity->x = n->x;
		stage.currentEntity->y = n->y;

		stage.routeHead.next = n->next;

		free(n);

		if (stage.routeHead.next == NULL)
		{
			((Unit*) stage.currentEntity->data)->ap--;

			resetAStar();
		}

		moveTimer = 5;
	}
}

We've added just one line. After we've determined that we've finished moving (Stage's routeHead's `next` is NULL), we are deducting an AP point from the current entity. Doing so here centralizes the movement logic, rather than attempting to put it in a number of other places (such as when the AI chooses where to walk).

We've also updated updateUnitRanges:


void updateUnitRanges(void)
{
	int x, y;
	Unit *u;

	u = (Unit*) stage.currentEntity->data;

	for (x = 0 ; x < MAP_WIDTH ; x++)
	{
		for (y = 0 ; y < MAP_HEIGHT ; y++)
		{
			stage.map[x][y].inMoveRange = 0;
		}
	}

	if (u->ap > 0)
	{
		testMoveRange(stage.currentEntity->x, stage.currentEntity->y, u->moveRange);
	}
}

Now, before calling testMoveRange, we're testing to see that the currently active unit has any AP points. If not, we'll not bother to call testMoveRange, meaning that all the map tiles in the stage will be inaccessible to them. Thus, the unit cannot move.

We've also added in a new function, called resetUnits:


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

	stage.currentEntity = NULL;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->type == ET_MAGE)
		{
			u = (Unit*) e->data;

			u->ap = u->maxAP;

			if (stage.currentEntity == NULL)
			{
				stage.currentEntity = e;

				updateUnitRanges();
			}
		}
	}
}

This function is responsible for restoring all the AP of our units in the stage. It loops through all the entities in the stage, looking for entities of type ET_MAGE. When we find one, we're extracting the unit data from its `data` field, and setting its `ap` to the value of its maxAP. In effect, we're just setting the AP back to its maximum. Note how we're also NULLing Stage's currentEntity at the start of the loop, before setting it to the first mage we find. This will be important later when we come to handling the AI turn. But more on that in a later part. We're also calling updateUnitRanges, so that the now-selected mage can immediately have positions to move to, without the need to deselect and re-select them (much like during the player's init phase).

Heading over to mages.c now, we've updated initMage:


Unit *initMage(Entity *e, char *name, char *filename)
{
	Unit *u;

	STRCPY(e->name, name);
	e->type = ET_MAGE;
	e->solid = 1;
	e->texture = getAtlasImage(filename, 1);

	u = initUnit(e);
	u->ap = u->maxAP = 2;

	return u;
}

We're setting all our mages' `ap` and maxAP to 2, so each mage will have 2 action points to spend each turn.

We're almost done! We just need a way to end our turn and also see our AP.

If we look at player.c, we've added updated the doPlayer function:


void doPlayer(void)
{
	doControls();

	doSelectUnit();

	doMoveUnit();
}

We're now calling a function named doControls. The function is very basic right now, but leaves room for expansion:


static void doControls(void)
{
	if (app.keyboard[SDL_SCANCODE_SPACE])
	{
		app.keyboard[SDL_SCANCODE_SPACE] = 0;

		endTurn();
	}
}

All we're doing is testing to see whether we've pressed Space, and calling endTurn if so.

The endTurn function itself is defined in stage.c:


void endTurn(void)
{
	stage.showRange = SHOW_RANGE_NONE;

	resetUnits();
}

The endTurn function does just two things: sets Stage's showRange to SHOW_RANGE_NONE, to clear the current display state, and also calls resetUnits. It's best to reset the display state, so things look neat when the next turn starts. Of course, options could be added to allow for the previous state to be retained or a different default state to be used instead.

Finally, let's look at the changes we've made to hud.c. We've updated drawHud:


void drawHud(void)
{
	if (!stage.animating)
	{
		drawSelectedTile();
	}

	drawTopBar();
}

We're calling a new function named drawTopBar:


static void drawTopBar(void)
{
	Unit *u;
	int x;
	char text[MAX_DESCRIPTION_LENGTH];

	drawRect(0, 0, SCREEN_WIDTH, 45, 0, 0, 0, 192);

	u = (Unit*) stage.currentEntity->data;

	x = 10;

	drawText(stage.currentEntity->name, x, 0, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	x += 200;
	sprintf(text, "AP: %d / %d", u->ap, u->maxAP);
	drawText(text, x, 0, 200, 200, 200, TEXT_ALIGN_LEFT, 0);
}

This function is very simple. We're first calling drawRect, with a transparent black colour, to darken a rectangular portion at the top of the screen. Next, we're extracting the unit data from the currently active entity, and calling drawText to display both their name, and current and maximum AP amounts.

There we have it! AP usage! We can now limit the number of actions a unit can take per turn, and also customize the amount of AP a unit can have. For example, one of the mages might have 3 AP instead of 2, allowing them to make more actions. This could be balanced in the game by also giving them fewer hit points, for example. Enemies could also have varying amounts of AP. A very powerful enemy might only have 1 AP.

As you can see now, the foundations we built in part 1 have allowed us to easily expand out our game. And since we can now end our turn and control our AP, it's about time we introduced some enemies. In the next part, we'll add in an AI ghost, who will move around whenever it is their turn.

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