« Back to tutorial listing

— A simple turn-based strategy game —
Part 2: Controlling multiple units

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

Introduction

Now that we have our movement and navigation setup, via A*, we can look into controlling multiple units. Most turn-based strategy games involve the player having command over several units, so in this part we're going to look into how we can switch between them.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS02 to run the code. You will see a window open like the one above, showing three wizards in a small room, surrounded by walls. Once again, you can click on any floor tile to command the currently selected wizard to move. Click on any other wizard to select and take control over them. You can also use the mouse wheel to cycle through the wizards. Once you're finished, close the window to exit.

Inspecting the code

As expected, this part is not nearly as long as part 1. Also, due to the foundations we laid, adding in support for controlling multiple characters is fairly straightforward.

Starting with player.c, we've updated doPlayer:


void doPlayer(void)
{
	doSelectUnit();

	doMoveUnit();
}

As well as doMoveUnit, we're now calling a new function called doSelectUnit:


static void doSelectUnit(void)
{
	Entity *e;

	if (app.mouse.buttons[SDL_BUTTON_LEFT])
	{
		e = getEntityAt(stage.selectedTile.x, stage.selectedTile.y);

		if (e != NULL)
		{
			app.mouse.buttons[SDL_BUTTON_LEFT] = 0;

			stage.currentEntity = e;
		}
	}

	if (app.mouse.buttons[SDL_BUTTON_X1])
	{
		app.mouse.buttons[SDL_BUTTON_X1] = 0;

		cyclePlayerUnits(-1);
	}

	if (app.mouse.buttons[SDL_BUTTON_X2])
	{
		app.mouse.buttons[SDL_BUTTON_X2] = 0;

		cyclePlayerUnits(1);
	}
}

This function is responsible for handling our unit selection. We start by checking if the left mouse button has been pressed. If so, we're going to make a call to a function called getEntityAt (more on this in a bit). We're going to pass in Stage's selectedTile's `x` and `y` to the function, and then evaluate the entity that is returned (`e`). If it's not NULL, we're going to cancel the left mouse button, and the set Stage's currentEntity as `e`.

Next, we're going to test the mouse wheels, SDL_BUTTON_X1 and SDL_BUTTON_X2. Depending on which detection the wheel is moved, we're going to call another new function called cyclePlayerUnits. We're going to feed in either -1 and 1, depending on the direction the wheel was moved, so we can essentially move forwards or backwards through our units. In both cases, we'll be cancelling the mouse wheel (by setting the relevant button index to 0), so that we don't cycle through the units at light speed!

Coming next to cyclePlayerUnits:


static void cyclePlayerUnits(int dir)
{
	int i;

	for (i = 0 ; i < NUM_PLAYER_UNITS ; i++)
	{
		if (units[i] == stage.currentEntity)
		{
			i += dir;

			if (i < 0)
			{
				i = NUM_PLAYER_UNITS - 1;
			}
			else if (i >= NUM_PLAYER_UNITS)
			{
				i = 0;
			}

			stage.currentEntity = units[i];

			return;
		}
	}
}

You might have been wondering why we were storing references to the wizards in an array called `units`, when they were already in the entity linked list. It's basically to make the task of cycling through them easier (there are plenty of other approaches we could've taken, of course, such as making our linked list include a reference to the previous entity).

As seen earlier, the cyclePlayerUnits function takes a single parameter: dir, which is the direction we wish to move through our `units` array. We start by setting up a for-loop, going from 0 to NUM_PLAYER_UNITS. This is to locate our currently active unit (currentEntity). Once we find them, we add the value of dir to i, then check to see if we've fallen outside of the bounds of the array, looping around to the start or end if so. Finally, we set Stage's currentEntity as the entity at the `units` array index of i, and return out of the function.

So, when cycling our units, we will first locate the currently active one, then move to the next or previous one, wrapping around as needed. Easy.

Moving next to addPlayerUnits, we've made some tweaks:


static void addPlayerUnits(void)
{
	Entity *e;
	int i, x, y;
	char *names[] = {"Andy", "Danny", "Izzy"};

	for (i = 0 ; i < NUM_PLAYER_UNITS ; i++)
	{
		e = initEntity(names[i]);

		e->side = SIDE_PLAYER;

		do
		{
			x = rand() % MAP_WIDTH;
			y = rand() % MAP_HEIGHT;
		}
		while (isBlocked(x, y));

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

		units[i] = e;
	}

	stage.currentEntity = units[0];
}

First of all, we're now adding in Danny and Izzy (NUM_PLAYER_UNITS is also now set as 3). As before, we're using a for-loop to create our wizards, but are assigning the random locations on the map to variables called `x` and `y`, rather than directly setting the entity's `x` and `y`. We'll see why this is done in a little bit. We're now also calling isBlocked in the do-while loop. This function has been updated, as we'll see next. Once we've found a valid location, we're setting the entity's `x` and `y` to the values of `x` and `y` that we set in the while loop.

Let's turn to entites.c, and look at isBlocked now:


int isBlocked(int x, int y)
{
	Entity *e;

	if (!isGround(x, y))
	{
		return 1;
	}

	e = getEntityAt(x, y);

	if (e != NULL && e != stage.currentEntity && e->solid)
	{
		return 1;
	}

	return 0;
}

This function used to only call isGround. However, we're now also making a call to getEntityAt, and evaluating the entity that is returned. The `x` and `y` position will be considered to be blocked if the returned entity is not null, is not the currentEntity, and is also solid. Otherwise, this function will return 0. This is why we're using temporary position variables in addPlayerUnits, as getEntityAt would always find an entity at its wanted position and the loop would never exit (we could set Stage's currentEntity as the entity we're placing, but that feels like a hack and will actually be problematic later on).

The getEntityAt function comes next:


Entity *getEntityAt(int x, int y)
{
	Entity *e, *rtn;

	rtn = NULL;

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->x == x && e->y == y)
		{
			if (e->solid)
			{
				return e;
			}

			rtn = e;
		}
	}

	return rtn;
}

This is quite a lot like the function found in the roguelike tutorial. We're looping through all our entities and seeing which ones match the `x` and `y` values passed in. If we find one that matches, and it is solid, we're returning it right away. Otherwise, we're assigning it to a variable called `rtn` (initally set to NULL). At the end of the function, we're returning `rtn`. The reason for this is because a solid entity is allowed to occupy a space where a non-solid entity also resides. In our game, solid entities will take priority, so we want to return the solid entities first, and non-solid ones later. There are some implication to this, such as more than one non-solid entity occupying a space and only one being returned, but this is not of concern in our game (and could easily be dealt with another way).

We're almost done! We've just got to update mages.c with our two new brave wizards. First, we'll make a small update to initMage:


void initMage(Entity *e, char *name, char *filename)
{
	STRCPY(e->name, name);
	e->type = ET_MAGE;
	e->solid = 1;
	e->texture = getAtlasImage(filename, 1);

	initUnit(e);
}

We're now passing through the wizard's name, as well as the filename, as this is common for all wizards (we've also updated initAndyMage to conform to this).

Next, we've added initDannyMage:


void initDannyMage(Entity *e)
{
	initMage(e, "Danny", "gfx/units/danny.png");
}

We're simply delegating to initMage, passing through a required name and texture filename.

initIzzyMage follows:


void initIzzyMage(Entity *e)
{
	initMage(e, "Izzy", "gfx/units/izzy.png");
}

Other than the parameters passed to initMage, it's identical. Both of these new functions are registered with our entity factory, so that the wizards can be created.

And that's it! A nice short part that shows how we can swap between our units and move them around the map. Next, we're going to look into how to limit the movement range of our units. Right now, they can move an unlimited distance, which wouldn't be too good in a strategy game. Each of our mages will have a different limit, to demonstrate how stats can vary.

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