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

Latest Updates

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

New tutorials
Tue, 11th May 2021

Orb source code
Sun, 25th April 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (6)
games (37)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (6)
water-closet (3)

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

— Simple 2D adventure game —
Part 4: Entity interactions, part 1

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

Introduction

It's time to introduce some other entities into our dungeon. We'll start with some inanimate objects, the type that are always found in dungeons: gold coins and chests! These items can be interacted with by walking into them. In the case of the gold coins, they will be picked up and added to our coin tally. In the case of the chest, there's nothing we can actually do with it because it's locked and we don't have a key a to open it.

Extract the archive, run make, and then use ./adventure04 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. Walk into the coins and the chest to trigger their interactions. Close the window to exit.

Inspecting the code

There are a number of ways in which we can go about handling entity interactions. In this game, we're going to be using function and data pointers in various places. We'll see this in action in a bit. For now, let's look at the updates to defs.h and structs.h we've made. Starting with defs.h:


enum {
	ALIVE_ALIVE,
	ALIVE_DEAD
};

enum {
	SOLID_NON_SOLID,
	SOLID_SOLID
};

We've added a enum to describe the liveness of an entity. ALIVE_ALIVE will be the default for any created entity, having a value of 0. The other states will be used to describe its other states. The other enum is the solid state of an entity. Again, by default an entity will be non-solid, having a value of 0.

Turning to structs.h, we've updated the Entity struct to add in some new members:


struct Entity {
	int x;
	int y;
	int facing;
	int alive;
	int solid;
	AtlasImage *texture;
	void (*data);
	void (*touch)(Entity *self, Entity *other);
	Entity *next;
};

The alive member we already covered. We've also added in solid. This will be used to flag whether an entity is passable or not. One entity cannot move into a square occupied by another solid entity. For example, a door, a rock, or other large object. The data member will be used to hold any piece of data that we want. As such, it can be used to extended the Entity by way of composition. We'll see this in action later. Finally, we've added in the touch function pointer. The function must be defined as a function that take two other entities as arguments. When calling this function, the first of these arguments will be the host entity itself (self). The second will be the thing that has touched it. Again, we'll see this in action later.

Also in structs.h, we've added in a Prisoner struct. Right now, this struct is very basic and only holds the amount of gold the prisoner is currently holding:


typedef struct {
	int gold;
} Prisoner;

With our structs and def tweaks made, we can take a look at how we're making use of them. Moving onto player.c, we've updated the initPlayer function:


void initPlayer(void)
{
	Prisoner *p;

	player = spawnEntity();

	player->x = 16;
	player->y = 14;
	player->solid = SOLID_SOLID;
	player->texture = getAtlasImage("gfx/entities/prisoner.png", 1);
	player->facing = FACING_RIGHT;

	p = malloc(sizeof(Prisoner));
	memset(p, 0, sizeof(Prisoner));
	player->data = p;

	movePlayer(0, 0);

	moveDelay = 0;
}

We're now declaring the player as solid (not that we're expecting anything to move into the player right now). We're also declaring a Prisoner pointer, as p, mallocing it, and setting it to the data pointer of the player entity.

While we're discussing the player, let's look at how the actual interaction will work. This is done in the movePlayer function:


static void movePlayer(int dx, int dy)
{
	int x, y;
	Entity *e;

	x = player->x + dx;
	y = player->y + dy;

	x = MAX(0, MIN(x, MAP_WIDTH - 1));
	y = MAX(0, MIN(y, MAP_HEIGHT - 1));

	if (dungeon.map.data[x][y] >= TILE_GROUND && dungeon.map.data[x][y] < TILE_WALL)
	{
		e = getEntityAt(x, y);

		if (e == NULL || e->solid == SOLID_NON_SOLID || e == player)
		{
			player->x = x;
			player->y = y;

			dungeon.camera.x = x;
			dungeon.camera.x -= (MAP_RENDER_WIDTH / 2);
			dungeon.camera.x = MIN(MAX(dungeon.camera.x, 0), MAP_WIDTH - MAP_RENDER_WIDTH);

			dungeon.camera.y = y;
			dungeon.camera.y -= (MAP_RENDER_HEIGHT / 2);
			dungeon.camera.y = MIN(MAX(dungeon.camera.y, 0), MAP_HEIGHT - MAP_RENDER_HEIGHT);

			moveDelay = 5;
		}

		if (e != NULL && e->touch != NULL)
		{
			e->touch(e, player);
		}
	}
}

Once we've established that the tile we're moving into isn't blocked by the map (it's not a hole or a wall), we want to discover if there's an entity in the square we wish to move to. If there isn't, or there is and it's not solid (or if it's the player themselves), we'll permit the movement. After checking the movement, we want the player to touch the entity in the square, regardless of whether we moved or not. We do this by calling the entity's touch, passing in itself and the player as arguments. What this done will depend entirely on the entity.

Note: in this game we don't have bridges or any entities that the player could walk over. If there were, we would want to test the entity collision before the world collision, so that we could determine if an entity that can be walked upon, before testing if it is blocked by the world. In effect, this would mean that a bridge entity sitting over a TILE_HOLE would permit movement.

Let's look at the entities we've defined for this tutorial, so we can see how each works. Starting with chest.c. This file contains all the details we need for creating a chest, including its touch response. The init function is simple:


void initChest(int x, int y)
{
	Entity *e;

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->texture = getAtlasImage("gfx/entities/chest.png", 1);
	e->solid = SOLID_SOLID;

	e->touch = touch;
}

The function is creating an entity, setting it's x and y (according to the arguments passed to the function), setting the texture, setting the chest's solid state to SOLID_SOLID, and then assigning the touch function pointer. Setting the chest's solid to SOLID_SOLID will mean that it will block the player's movement when they try to walk into it. However, the touch function will still be called. If we look at the touch function pointer, we can see it's also very simple:


static void touch(Entity *self, Entity *other)
{
	if (other == player)
	{
		setInfoMessage("It's locked and I don't have a key.");
	}
}

The function merely checks if the entity that has touched it (other) is the player (a global variable) and is so calls the setInfoMessage function, stating that the chest is locked. Note how the signature of the touch function itself conforms to that we defined in the Entity struct. We wouldn't be allowed to assign it if this wasn't the case.

The other entities we've created for this tutorial are also very simple. Let's look at goldCoin.c next. This file has just two functions, an init and a touch. The initGoldCoin function isn't too different from initChest:


void initGoldCoin(int x, int y)
{
	Entity *e;

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->texture = getAtlasImage("gfx/entities/goldCoin.png", 1);

	e->touch = touch;
}

For the coin, we're assigning a different texture and also leaving the solid flag as 0 (SOLID_NON_SOLID), meaning it won't block player movement. The assigned touch function is a little more complicated, but nothing that can't be easily understood:


static void touch(Entity *self, Entity *other)
{
	Prisoner *p;

	if (other == player)
	{
		p = (Prisoner*) other->data;

		p->gold++;

		self->alive = ALIVE_DEAD;

		setInfoMessage("Picked up a gold coin.");
	}
}

Again, we want to only react to the touching entity being the player. If so, we extract the Prisoner data from other->data, increment the value of gold by 1, tell the entity it is now dead, and set an information message to say we've picked up a gold coin. Setting the alive state of the gold coin to ALIVE_DEAD will mean that we will remove it when we process our entities (more on this in a bit).

The last entity is defined in goldCoins.c (plural). This is very similar to goldCoin.c, except that we'll be awarding the player more coins and using a different texture:


void initGoldCoins(int x, int y)
{
	Entity *e;

	e = spawnEntity();
	e->x = x;
	e->y = y;
	e->texture = getAtlasImage("gfx/entities/goldCoins.png", 1);

	e->touch = touch;
}

The information message is different in the touch function:


static void touch(Entity *self, Entity *other)
{
	Prisoner *p;

	if (other == player)
	{
		p = (Prisoner*) other->data;

		p->gold += 5;

		self->alive = ALIVE_DEAD;

		setInfoMessage("Found 5 gold coins!");
	}
}

Of course, this could have been one entity, with different init parameters. We're only doing it this way right now to demonstrate how the entity processing and setup behaves.

Moving on to entities.c, we've introduced a new function called doEntities, which will be used to process our entities each frame:


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

	prev = &dungeon.entityHead;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->alive == ALIVE_DEAD)
		{
			if (e == dungeon.entityTail)
			{
				dungeon.entityTail = prev;
			}

			if (e->data != NULL)
			{
				free(e->data);
			}

			prev->next = e->next;
			free(e);
			e = prev;
		}

		prev = e;
	}
}

doEntities iterates through each of the entities in the dungeon, checking their alive state. If the entity is dead (ALIVE_DEAD), we want to remove it. We'll do this by removing the entity from the linked list (assigning the previous entity's next to the dead entity's next) and freeing the memory. We also need to free the entity's data if there is any, to avoid a memory leak. Another thing we need to do is test if the entity to be removed was the tail of the linked list, and, if so, reassign this to the previous entity in the loop. If we fail to do this, the tail will be pointing at invalid memory, meaning the game would crash when trying to add a new entity.

The draw function has received some tweaks:


void drawEntities(void)
{
	Entity *e;
	int x, y;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		x = e->x - dungeon.camera.x;
		y = e->y - dungeon.camera.y;

		if (x >= 0 && y >= 0 && x < MAP_RENDER_WIDTH && y < MAP_RENDER_HEIGHT)
		{
			x = (x * TILE_SIZE) + (TILE_SIZE / 2);
			y = (y * TILE_SIZE) + (TILE_SIZE / 2);

			x += dungeon.renderOffset.x;
			y += dungeon.renderOffset.y;

			blitAtlasImage(e->texture, x, y, 1, e->facing == FACING_LEFT ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL);
		}
	}
}

Now, before drawing the entity, we want to find out if the entity's x and y values, after being adjusted by the camera position, lie within the rendering bounds of the map. If we didn't do this test, entities outside of the view would be drawn and this would look strange. A minor tweak, but an important one.

The final new function in entities.c if the getEntityAt function that we saw coming into use during the player movement:


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

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

	return NULL;
}

All this function does is loop through all the entities in the dungeon and return the one found at the requested coordinates. If no entity is found at that location, we simply return NULL.

We're almost done with this update, so let's look at some other tweaks that have been made before finishing up. Turning to dungeon.c, we've updated the initDungeon function:


void initDungeon(void)
{
	initMap();

	initEntities();

	initPlayer();

	initHud();

	initGoldCoin(18, 17);

	initGoldCoin(23, 14);

	initGoldCoins(56, 0);

	initChest(12, 12);

	dungeon.renderOffset.x = (SCREEN_WIDTH - (MAP_RENDER_WIDTH * TILE_SIZE)) / 2;
	dungeon.renderOffset.y = 20;

	app.delegate.logic = &logic;
	app.delegate.draw = &draw;
}

We're now making calls to initGoldCoin, initGoldCoins, and also initChest, passing over the coordinates we want to add the entities at. We're hardcoding these right now, to keep things easy to understand. In a later tutorial, we'll look into how to load all the entities and map data as a single package.

Finally, let's take a quick look at hud.c. This is where the info messages are handled (such as touching a chest or picking up coins). The file is naturally pretty simple. Starting with the initHud function:


void initHud(void)
{
	memset(infoMessage, 0, sizeof(infoMessage));

	infoMessageTimer = 0;
}

The infoMessage is a char array that we are zeroing with memset. We're also setting a variable called infoMessageTimer to 0. This timer is used to control how long the info message is displayed for.

If we look at doHud (which is called by the dungeon's logic step) we can see that we are decrementing the value of infoMessageTimer:


void doHud(void)
{
	infoMessageTimer = MAX(infoMessageTimer - app.deltaTime, 0);
}

We're also using the MAX macro here, just to prevent the value from falling too long and wrapping. Although this would take a very, very, very long time, it's still good practice. The drawHud function is where we render the info message and the details about the gold carried:


void drawHud(void)
{
	char message[64];

	if (infoMessageTimer > 0)
	{
		drawText(infoMessage, 10, SCREEN_HEIGHT - 50, 255, 255, 255, TEXT_ALIGN_LEFT, 0);
	}

	sprintf(message, "Gold: %d", ((Prisoner*)player->data)->gold);

	drawText(message, SCREEN_WIDTH - 15, SCREEN_HEIGHT - 50, 255, 200, 32, TEXT_ALIGN_RIGHT, 0);
}

As expected, the info message is only displayed if infoMessageTimer is greater than 0. As for the gold, we're extracting this from the player's data value, casting it to a Prisoner, and getting the value of gold that way. In both cases, we're calling our drawText function, defined in text.c. You may wish to refer to the SDL2 TTF tutorial for more information on how that all works.

The final function in hud.c is setInfoMessage, used for setting the information message (as called by chest.c, etc):


void setInfoMessage(char *message)
{
	infoMessageTimer = FPS * 2.5;

	STRCPY(infoMessage, message);
}

When this function is called, it resets the message display timer to 2.5 seconds and also copies the message input into our infoMessage array. We don't need to worry about an overflow here, as the macro STRCPY automatically handles that for us.

That's our entity interactions covered. There is more we can do, but we've got the ability to interact with them, which gives our a lot of flexability. Next, we'll look at handling a very basic inventory.

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

Comments

Mobile site