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
SDL 1 tutorials (outdated)

Latest Updates

SDL2 isometric tutorial
Sun, 24th July 2022

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

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 (11)
water-closet (4)

Books


Alysha

When her village is attacked and her friends and family are taken away to be sold as slaves, Alysha Tanner sets out on a quest across the world to track them down and return them home. Along the way, she is aided by the most unlikely of allies - the world's last remaining dragon.

Click here to learn more and read an extract!

« Back to tutorial listing

— An old-school isometric game —
Part 8: Entities and items

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

Introduction

It's about time we populated the world with more interesting things, rather than just glasses of water and trees. So, in this part we're going to throw in a number of new items to collect. What will make this interesting is that the entities will have some different properties, that we will need to handle when rendering.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./isometric08 to run the code. You will see a window open like the one above. Click on ground squares to have Purple Guy move around. The world now contains various items, that can be picked up: glasses, spills, and utensils. To collect them, simply have Purple Guy walk over them. The stats in the bottom-left will increase as you do so. Note that it may not be possible to collect all the items, due to random placement and parts of the map being inaccessible. Once you're finished, close the window to exit.

Inspecting the code

We're going to focus this part on handling our entities, with loading, placement, and interactions all considered. We've tweaked and added in some entities types, but we won't cover them all, as some things are very similar in nature.

You might be wondering what all these things are littering the map. Well, the plot of our game is that Purple Guy works as a "cleaner", and has been summoned to a park where a "job" has gone slightly wrong. It's therefore his task to tidy up and make good. He needs to collect all the "red boxes", "utensils", and wash down the "spills". Washing down the spills requires water, for which he can use the glasses of water that are standing about. Yep, the world of "Three Guys" is quite odd 😉

So, diving right in, we'll start with defs.h:


#define EF_SHADOW                 (2 << 1)

We've added a new entity flags, EF_SHADOW. This is a flag to say that this entity will have a shadow. Not all of our entities will have a shadow, since, due to their texture and size, it won't be visible. Purple Guy was previously always drawn with a shadow, but we can't see it!

Moving onto structs.h now:


struct Entity {
	char name[MAX_NAME_LENGTH];
	int x;
	int z;
	int base;
	int layer;
	int dead;
	unsigned long flags;
	unsigned long isoFlags;
	AtlasImage *texture;
	void (*touch)(Entity *self, Entity *other);
	Entity *next;
};

We've updated Entity, with several new fields. `layer` is the layer that this entity will be rendered at. Not all our entities will now be drawn at LAYER_FOREGROUND, so we'll be able to set them individually this way. `dead` is a flag to say whether the entity is dead and should be removed from the world. Lastly, `touch` is a function pointer to handling what happens when one entity touches another. In this game, it will always be Purple Guy touching something else, as we'll see later on.

We've next updated World:


typedef struct {
	MapTile map[MAP_SIZE][MAP_SIZE];
	Entity entityHead, *entityTail;
	Node routeHead;
	Entity *player;
	SDL_Rect playerISORect;
	int utensils, totalUtensils;
	int redBoxes, totalRedBoxes;
	int spills, totalSpills;
	int water;
	struct {
		int x;
		int z;
	} cursor;
	struct {
		int x;
		int z;
	} camera;
} World;

We've added in a number of new fields here: `utensils` is the number of utensils found, while totalUtensils is the total on the map overall. redBoxes is the number of red boxes found, totalRedBoxes being the total on the map. `spills` and totalSpills are the number of spills and total of, while `water` is the amount of water at Purple Guy's disposal.

Nothing taxing there, just some fields to record various stats.

Now, let's look at the updates we've made to glass.c:


void initGlass(Entity *e)
{
	e->layer = LAYER_FOREGROUND;
	e->texture = getAtlasImage("gfx/entities/glass.png", 1);
	e->base = -1;
	e->flags = EF_SHADOW;

	e->touch = touch;
}

We're now setting the glass's `layer` field, to LAYER_FOREGROUND (essentially, it will remain unchanged to how it was previously being drawn). We're also setting its `flags` to EF_SHADOW, so that it can have a shadow. We're then setting its `touch` field to the touch function we'd added:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		world.water += 5;

		self->dead = 1;
	}
}

The `touch` function is something we've seen many times before in previous tutorials. We're first checking to see if `other` is the player. If so, we're going to add 5 to World's `water`, and set the glass's (`self`) `dead` flag to 1, to remove it from the world.

Pretty simple. Now, let's turn to knife.c, a new file. It contains two functions, and is quite similar to glass.c. Starting with initKnife:


void initKnife(Entity *e)
{
	char filename[MAX_FILENAME_LENGTH];

	sprintf(filename, "gfx/entities/knife%d.png", 1 + rand() % 4);

	e->layer = LAYER_MID;
	e->texture = getAtlasImage(filename, 1);
	e->base = -8;

	e->touch = touch;

	world.totalUtensils++;
}

Our knives (utensils) can have 4 different images, so we're randomly selecting a graphic, applied to `filename`. Next, we're setting the knife's (`e`'s) data. The knife's `layer` is set to LAYER_MID. This is because it is sitting on the ground, and can be covered by other things, such as Purple Guy. While knives are collected as soon as they are touched, some other entities (such as spills) aren't. Setting the layer as LAYER_MID is most appropriate here. We're also setting the knife's `touch` field to the function in the file, and then incrementing World's totalUtensils value. This keeps our totalUtensils value in sync with the number of knives we've created.

Next, we come to `touch`:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		world.utensils++;

		self->dead = 1;
	}
}

Like the touch function in glass.c, we're testing if the thing (`other`) touching the knife is the player, and then increment World's `utensils` value, and set the knife's `dead` flag to 1, to remove it.

If we take a look at redBox.c next, we will find mostly the same thing. Starting with initRedBox:


void initRedBox(Entity *e)
{
	char filename[MAX_FILENAME_LENGTH];

	sprintf(filename, "gfx/entities/redCube%d.png", 1 + rand() % 7);

	e->layer = LAYER_FOREGROUND;
	e->texture = getAtlasImage(filename, 1);
	e->base = -10;

	e->touch = touch;

	world.totalRedBoxes++;
}

Once more, we're selecting from a number of different textures, setting the entity's `layer` type (this time LAYER_FOREGROUND), setting the `touch` field, and also incrementing World's totalRedBoxes.

The `touch` function is also quite familiar:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player)
	{
		world.redBoxes++;

		self->dead = 1;
	}
}

When touched by the player, we increment World's redBoxes, and set the Redbox's `dead` flag to 1.

Finally, onto spill.c. Again, initSpill isn't doing anything special:


void initSpill(Entity *e)
{
	char filename[MAX_FILENAME_LENGTH];

	sprintf(filename, "gfx/entities/spill%d.png", 1 + rand() % 5);

	e->layer = LAYER_MID;
	e->texture = getAtlasImage(filename, 1);

	e->touch = touch;

	world.totalSpills++;
}

The major thing to note is that we're incrementing World's totalSpills.

The `touch` function, however, is doing something a little different:


static void touch(Entity *self, Entity *other)
{
	if (other == world.player && world.water > 0)
	{
		world.spills++;

		world.water--;

		self->dead = 1;
	}
}

When we touch a spill, we need water to clear it up. We therefore test that the thing touching the spill is the player, and also that World's `water` value is greater than 0. We then increment World's `spills`, decrement `water` by 1, and also set the spill's (`self`) `dead` flag to 1.

Okay, time to move on. Let's turn our attention to entities.c, where we've made a bunch of changes. Starting with initEntities:


void initEntities(void)
{
	memset(&world.entityHead, 0, sizeof(Entity));
	world.entityTail = &world.entityHead;

	loadEntities();

	addEntities();

	shadowTexture = getAtlasImage("gfx/misc/shadow.png", 1);
}

We're now calling loadEntities, which we'll see a bit later on. Now onto doEntities:


void doEntities(void)
{
	Entity *e, *prev;

	prev = &world.entityHead;

	for (e = world.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->dead)
		{
			prev->next = e->next;

			free(e);

			e = prev;
		}

		prev = e;
	}
}

In this function, we're looping through all the entities, testing if their `dead` flag is set, and removing them from our linked list if so. If you've looked at previous tutorials, you may have seen these entities being pushed into a "dead list". This was done to guard against NULL pointers, if something happens to have a reference to the entity we're removing. There is no danger of that in this game, so we're free to simply call free on the entity we're removing.

Next up, let's look at drawEntities:


void drawEntities(void)
{
	Entity *e;
	int x, z, sx, sy;

	for (e = world.entityHead.next ; e != NULL ; e = e->next)
	{
		if (isWithinISOScreen(e->x, e->z))
		{
			x = e->x - world.camera.x;
			z = e->z - world.camera.z;

			if (e->layer == LAYER_FOREGROUND)
			{
				sx = TILE_WIDTH / 2;
				sx -= e->texture->rect.w / 2;

				sy = TILE_HEIGHT / 2;
				sy -= e->texture->rect.h;
				sy -= e->base;

				addISOObject(x, z, sx, sy, e->texture, LAYER_FOREGROUND, e->isoFlags);

				if (e == world.player)
				{
					world.playerISORect.w = world.player->texture->rect.w;
					world.playerISORect.h = world.player->texture->rect.h - 8;

					toISO(x, z, &world.playerISORect.x, &world.playerISORect.y);
					world.playerISORect.x += sx;
					world.playerISORect.y += sy;
				}
			}
			else
			{
				addISOObject(x, z, 0, e->base, e->texture, e->layer, e->isoFlags);
			}

			if (e->flags & EF_SHADOW)
			{
				sx = TILE_WIDTH / 2;
				sx -= shadowTexture->rect.w / 2;

				sy = -shadowTexture->rect.h;
				sy += TILE_HEIGHT;

				addISOObject(x, z, sx, sy, shadowTexture, LAYER_MID, IF_NONE);
			}
		}
	}
}

A couple of changes here. First, we're testing whether the entity's layer is LAYER_FOREGROUND, and then only applying the logic to handle our position adjustments (sx and sy) if so. Otherwise, we're calling addISOObject and passing over the entity's `layer` value, as well as their `base` value for the ISOObject's sy, to allow us to make position adjustments. The other change is that we're testing if the entity's `flags` contains EF_SHADOW. If so, we're drawing the shadow as normal (so, spills and knives, for example, will not have a shadow added).

Now for loadEntities:


static void loadEntities(void)
{
	Entity *e;
	char *text, *type;
	cJSON *root, *node;

	text = readFile("data/entities.json");

	root = cJSON_Parse(text);

	for (node = root->child ; node != NULL ; node = node->next)
	{
		type = cJSON_GetObjectItem(node, "type")->valuestring;

		e = initEntity(type);

		e->x = cJSON_GetObjectItem(node, "x")->valueint;
		e->z = cJSON_GetObjectItem(node, "z")->valueint;
		STRCPY(e->name, cJSON_GetObjectItem(node, "name")->valuestring);
	}

	cJSON_Delete(root);

	free(text);
}

This is a new function that loads entity data from a file. The reason for this is because we want some entities in the game to be of fixed location (Purple Guy, trees), while others can be placed wherever they like (red boxes, spills, knives). We want to ensure that the random placement of trees doesn't end up blocking our path, for example.

Our entity JSON data lives in data/entities.json, and looks like this:

[
  {
    "type": "purpleGuy",
    "name": "Purple Guy",
    "x": 14,
    "z": 8
  },
  {
    "type": "barrel",
    "name": "Barrel",
    "x": 59,
    "z": 60
  },
  {
    "type": "tree",
    "name": "Tree",
    "x": 84,
    "z": 9
  },
  ...
]

Loading the data is easy, and something we've seen in a number of other tutorials, so we'll not linger too long. In short, we're opening the JSON file and parsing it. Then, for each item in the JSON array, we're creating an Entity of the type specified with a call to initEntity, and then setting its `x` and `z` values, and the `name`. Nothing out of the ordinary!

The placeRandom function has also been updated, but is now a little more complicated:


static void placeRandom(int *x, int *z)
{
	int ok;

	do
	{
		*x = rand() % MAP_SIZE;
		*z = rand() % MAP_SIZE;

		ok =
		*x % MAP_RENDER_SIZE != 0 &&
		*z % MAP_RENDER_SIZE != 0 &&
		*x % MAP_RENDER_SIZE != (MAP_RENDER_SIZE - 1) &&
		*z % MAP_RENDER_SIZE != (MAP_RENDER_SIZE - 1) &&
		isGround(*x, *z) &&
		getEntityAt(*x, *z) == NULL;
	}
	while (!ok);
}

So, what's going on here? To begin with, we're randomly picking a location on the entire map, as before. However, for both `x` and `z` we're testing the modulo of MAP_RENDER_SIZE, to ensure neither are 0 or MAP_RENDER_SIZE - 1. This is to keep the randomly selected position away from the reserved edges of individual zones. We don't want entities to be spawn in on the exit points, and so we need to test for this. We're also checking the `x` and `z` coordinates we've chosen are a ground tile, and that there is not already an entity occupying the position (via a call to getEntityAt).

All in all, this is to both make sure we can access the objects, and also that they were added to the map in a tidy fashion.

With that done, it's over to player.c, where we've updated doPlayer:


void doPlayer(void)
{
	Node *n;
	Entity *other;
	int x, z, dx, dz, facing;

	walkTimer = MAX(walkTimer - app.deltaTime, 0);

	if (world.routeHead.next == NULL)
	{
		// snipped
	}
	else if (walkTimer == 0)
	{
		n = world.routeHead.next;

		other = getEntityAt(n->x, n->z);

		if (other != NULL && other->touch != NULL)
		{
			other->touch(other, world.player);
		}

		// snipped
	}
}

We've made just one change here. Before moving Purple Guy to the next node (`n`), when walking, we call getEntityAt, passing over the node's `x` and `z` values, and assigning the result to `other`. If `other` is not NULL and also has a `touch` function assigned, we know we're good to interact with it. We therefore call `other`'s `touch` function, passing over `other` and the player. As we saw in entities such as the glass, knife, and spill, the various pieces of logic will kick in.

Notice how we're calling this touch function before moving. This is because our code only expects to find a single entity occupying a given location at any one time. If we moved Purple Guy and then asked for the entity at the node's position, it would always return Purple Guy; the getEntityAt function returns the first entity it finds in the linked list at the requested coordinates. Purple Guy is the first thing in our world, so would always be returned, making it impossible to pick things up.

Phew! We're very nearly done. Just a couple more changes and this part is finished.

Over to world.c now, where we've tweaked `logic`:


static void logic(void)
{
	doISOObjects();

	doEntities();

	doMap();

	doCursor();

	doPlayer();
}

We've simply added a call to doEntities, the new function we added.

Finally, over to hud.c, where we've updated drawInfo:


static void drawInfo(void)
{
	char text[64];
	int y;

	sprintf(text, "%d,%d", world.cursor.x + world.camera.x, world.cursor.z + world.camera.z);
	drawText(text, 25, 50, 255, 255, 255, TEXT_ALIGN_LEFT, 0);

	y = SCREEN_HEIGHT - 55;

	sprintf(text, "Red boxes: %d / %d", world.redBoxes, world.totalRedBoxes);
	drawText(text, 25, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y -= 40;

	sprintf(text, "Spills: %d / %d", world.spills, world.totalSpills);
	drawText(text, 25, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y -= 40;

	sprintf(text, "Utensils: %d / %d", world.utensils, world.totalUtensils);
	drawText(text, 25, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y -= 40;

	sprintf(text, "Water: %d", world.water);
	drawText(text, 25, y, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	y -= 40;
}

We're now drawing four text strings, to show the progress of our game. Text for Red boxes, Spills, Utensils, and Water are all displayed, using a combination of sprintf and the drawText function. To make things a little easier, we're setting a variable called `y` to 55 pixels above the bottom of the screen, and then decrease the value of `y` by 40 for each piece of text we draw (we're of course using `y` as the y position in drawText).

Another part down! Yet again, it looks as though as though our game is more or less done. However, there are still parts of the map that are inaccessible to us. If items appear there, we can't pick any of them up. So, in the next part we're going to look at adding in bridges, to allow us to reach these locations. Of course, to make it a little more interesting, we're going to need to raise these bridges by stepping on buttons.

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 59.01MB 24th July 2022

Click here to see the list of files in the archive

Mobile site