« Back to tutorial listing

— Simple 2D adventure game —
Part 3: Loading a map

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

Introduction

Now we can move around our map and be constrined by the tiles (cannot cross holes or walls), it's time we worked with a proper map. The best map we have up to this point contains just random scatters of holes and walls. Loading map data from a file is easy, as it's a list of number, enough to fill our map array.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./adventure03 to run the code. You will see a window open like the one above, showing the prisoner on a tiled background. Use WASD to move around. You will find this map more interesting to explore, as it's more maze-like in nature. Close the window to exit.

Inspecting the code

Only two files have changed in this update: map.c and player.c. Before we look at those changes, let's take a look at the map data itself:

1 1 1 1 1 40 40 1 1 1 1 40 0 40 40 40 40 40 40 0 0 0 0 40 40 40 1 1 40 0 0 0 0 0 0 40 40 40 40 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 40 1 1 1 1 40 0 0 0 40 40 1 1 1 1 40 40 40 0 0 0 0 40 1 1 40 40 40 40 1 1 1 1 1 1 1 1 40 40 40 40 40 40 40 1 1 1 40 40 40 40 1 1 1 1 40 0 40 1 1 1 1 40 40 40 40 40 1 1 1 1 1 1 1 40 40 0 0 0 40 1 1 1 1 1 1 1 40 40 40 1 1 1 1 40 0 0 0 0 0 0 1 1 1 40 0 0 40 1 1 1 1 40 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 40 1 40 40 40 40 1 1 40 0 40 1 1 1 1 40 40 40 0 0 0 0 1 1 1 40 0 0 40 40 40 40 40 40 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 40 1 40 40 40 40 1 1 40 0 40 1 1 1 1 1 1 40 40 0 0 0 1 1 1 40 0 0 0 0 0 0 0 0 0 40 1 1 1 1 1 1 40 40 40 40 40 40 1 1 1 1 1 40 0 0 0 40 40 40 40 1 1 1 1 40 0 40 1 1 1 1 1 1 1 40 0 0 0 1 1 1 40 40 40 40 40 40 40 40 40 40 40 1 1 40 1 1 1 40 0 0 0 0 40 1 1 1 1 1 40 0 0 0 0 0 0 40 40 40 40 40 40 0 40 1 1 1 1 1 1 1 40 40 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 1 1 1 40 0 1 1 0 40 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 1 1 1 1 40 40 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 1 1 1 40 0 1 1 0 40 1 1 1 1 1 40 0 40 40 40 40 40 40 40 40 40 40 40 40 40 1 1 40 40 40 40 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 1 1 1 40 0 1 1 0 40 1 1 1 1 1 40 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 40 1 1 1 1 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 1 1 40 40 1 1 1 1 1 40 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 40 40 40 40 40 40 0 0 0 0 0 0 1 1 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 40 40 40 40 40 40 40 1 1 1 1 1 1 1 40 40 0 0 0 0 0 0 0 0 40 0 40 40 40 40 0 1 1 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 40 40 40 40 1 1 1 1 40 0 0 0 40 40 40 40 40 40 40 0 40 1 1 40 0 1 1 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 40 0 0 0 40 1 1 1 1 1 40 0 40 1 1 40 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 40 0 0 0 40 1 1 1 1 1 40 0 40 1 1 40 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 40 40 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 40 0 0 0 40 40 40 1 1 1 40 1 1 1 1 1 1 1 40 40 40 40 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 40 0 0 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 40 0 0 0 0 0 40 1 1 1 40 1 1 1 1 1 1 1 40 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 0 0 0 40 1 1 1 1 40 40 40 0 0 0 40 1 1 1 40 1 1 1 1 1 1 1 40 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 0 40 40 40 1 1 1 1 1 1 40 0 0 0 40 1 1 1 40 1 1 1 1 1 40 40 40 0 0 0 40 40 40 40 40 40 40 40 40 40 40 40 40 40 40 1 40 0 0 0 0 0 40 40 40 40 40 40 40 1 1 1 1 1 1 1 1 40 0 0 0 40 1 1 1 40 40 40 1 1 1 40 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 40 1 40 40 40 40 40 40 40 1 1 1 1 1 1 1 40 40 40 40 40 40 40 40 0 0 0 40 1 1 1 40 1 1 1 1 1 40 0 0 0 40 40 40 40 40 40 40 40 40 40 40 40 0 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 0 0 0 0 0 0 40 1 1 1 40 1 1 1 1 1 40 0 0 0 40 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 40 40 40 0 0 0 40 40 40 40 40 1 1 1 40 1 1 1 1 1 40 0 0 40 40 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 40 1 1 1 1 1 1 1 40 1 1 1 1 1 40 40 40 40 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 40 1 1 40 40 40 40 40 40 40 40 40 40 40 40 40 1 1 1 40 0 0 0 40 40 40 40 40 40 1 1 40 1 1 1 1 1 1 1 1 1 1 40 40 40 40 40 40 40 1 1 1 40 40 0 0 40 40 1 1 40 0 0 0 0 0 0 0 0 0 0 0 40 1 1 1 40 0 0 0 0 0 0 0 0 40 1 1 1 1 1 40 40 1 40 40 1 1 1 40 0 0 0 0 0 40 1 1 1 1 40 40 40 40 1 1 1 40 0 0 0 0 0 0 0 0 0 0 0 40 40 1 1 40 40 0 0 0 0 0 0 0 40 1 1 1 1 1 40 0 1 0 40 1 1 1 1 1 1 1 1 0 40 1 1 1 1 1 1 1 1 1 1 1 40 40 40 40 40 40 40 40 40 0 0 0 0 40 1 1 1 40 0 0 0 0 0 0 0 40 1 1 1 1 1 40 0 1 0 40 1 1 1 40 0 1 1 1 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 40 0 0 0 0 40 1 1 1 40 40 40 40 40 40 40 40 40 1 1 1 1 1 40 0 0 0 40 1 1 1 40 0 1 1 1 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 40 0 0 0 0 40 1 1 1 1 1 1 1 1 1 1 1 1 1 1

As I said, it's just one long list of numbers. In this case, 1710..! Quite a lot, and our map is only 57 x 30. The map for the final dungeon will be bigger. Still, the file size of this map is just under 4kb, so it's not terrible. This map wasn't produced by hand. A simple map editor was rolled to help with creating it. A future tutorial will look into how to make such an editor. Now let's look at the code:

Starting with map.c, the initMap function now calls loadMap, to load the map data, before then calling another function called decorateMap to add details:


void initMap(void)
{
	loadTiles();

	loadMap();

	decorateMap();
}

The loadMap function itself is simple:


static void loadMap(void)
{
	int x, y;
	char *data, *p;

	data = readFile("data/map.data");

	p = data;

	for (y = 0 ; y < MAP_HEIGHT ; y++)
	{
		for (x = 0 ; x < MAP_WIDTH ; x++)
		{
			dungeon.map.data[x][y] = atoi(p);

			do {p++;} while (*p != ' ');
		}
	}

	free(data);
}

We call a function called readFile, to load the map data into a char array, then point another char pointer (p) at the start of the array. Now ready to load our map data, we set up y and x variables to read the data by row and column; so, we'll read all the values on the first row, then the second row, etc., until we have read all the data. We call atoi on p (which will initially be pointing at the first number in our map data), to read the tile value and assign it to the appropriate index in our map. After this, we'll increment the value of p while the character isn't a space, to move onto the next number. Our numbers are space delimited, and so setting up this while loop will jump to the next one. Remember, calling atoi will read the next number found, but won't move the pointer on! If we didn't do this, we would end up reading the same number constantly and never finish. If we told the pointer to move on just one character, numbers composed of more than one digit would cause the data to read incorrectly. Once we're finished reading the data, we free it.

Note that that loadMap function assumes there are exactly the right number of values and performs no error checking or correction..!

Onto the decorateMap function. This function serves to randomly change the ground and walls tiles:


static void decorateMap(void)
{
	int x, y;

	srand(144893);

	for (x = 0 ; x < MAP_WIDTH ; x++)
	{
		for (y = 0 ; y < MAP_HEIGHT ; y++)
		{
			if (dungeon.map.data[x][y] == TILE_GROUND && rand() % 5 == 0)
			{
				dungeon.map.data[x][y] += (1 + rand() % 4);
			}

			if (dungeon.map.data[x][y] == TILE_WALL)
			{
				dungeon.map.data[x][y] += rand() % 6;
			}
		}
	}
}

We're seeding the random with a fixed number, so that the random picks are always the same, and then looping through our map data. If we encounter a ground tile, there's a one in five chance we'll change it. Effectively, this step turns some normal ground tiles into cracked tiles. We know from our atlas that the four cracked tiles follow directly after the regular ground tile, which is why we increment the number by 1-4. Similarly, when we encounter a wall tile we'll randomly add 0-5 to the value. This will either leave the wall as default or change it to one of the other wall image types.

There's no real reason for not manually doing this in the map data itself, other than to do away with the repetitive nature of doing so in an editor, where the process could take some time.

Moving on to player.c, we've made just some simple adjustment to the player's starting position. Instead of starting at 0,0, we're moving the player over to 14,28. This is just to give them a new starting position to start exploring from.


void initPlayer(void)
{
	player = spawnEntity();

	player->x = 14;
	player->y = 28;
	player->texture = getAtlasImage("gfx/entities/prisoner.png", 1);
	player->facing = FACING_LEFT;

	movePlayer(0, 0);

	moveDelay = 0;
}

Before we finish, a quick look at the readFile function, which lives in util.c. This function simlpy reads a file into a char array and returns it. If the file doesn't exist, we print an error and exit the program. Because the data read is malloc'd, need to always free it when we're done using it (as happens in loadMap).


char *readFile(char *filename)
{
	char *buffer;
	long length;
	FILE *file;

	file = fopen(filename, "rb");

	if (file == NULL)
	{
		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL, "No such file '%s'", filename);
		exit(1);
	}

	fseek(file, 0, SEEK_END);
	length = ftell(file);
	fseek(file, 0, SEEK_SET);

	buffer = malloc(length);
	memset(buffer, 0, length);
	fread(buffer, 1, length, file);

	fclose(file);

	return buffer;
}

So, there we have it - a basic map loading routine. We'll be using this map for a few more tutorials, but the final dungeon that will form our core game will actually be a few times larger; consider this map to be a test bed for various bits and pieces to come.

Next, we'll look at how we can handle interacting with other entities, such as items.

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