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

Desktop site