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

Latest Updates

SDL2 Rogue tutorial
Wed, 29th September 2021

SDL2 Gunner tutorial
Thu, 26th August 2021

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 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 (8)
water-closet (3)

Books

« Back to tutorial listing

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

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

Introduction

In the last tutorial, we worked on simple inventory management. With that done, we can expand our gameplay somewhat. In this tutorial, we'll see how we can use an inventory item (a rusty key) to interact with a dungeon element (a chest), to receive the object contained within.

Extract the archive, run make, and then use ./adventure06 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. The two chests are locked and require a key to open (the key can be found nearby). Pick up the key and then walk into one of the chests to open it. The key will disappear from your inventory and be replaced by the item in the chest - either an eyeball or a red potion. Only one key exists in the dungeon, so choose wisely! Close the window to exit.

Inspecting the code

Compared to the last tutorial, this one is thankfully much shorter. We'll start by looking at structs.h, where we've introduced a new struct:


typedef struct {
	int isOpen;
	Entity *item;
} Chest;

The Chest struct is used to detail a chest, in the same way as the Prisoner struct details the prisoner. It holds the open state of the chest (isOpen) and also a pointer to the entity that will be the item within.

We've made updates to chest.c, to use the new Chest struct, assigning an instance to the chest entity's data pointer:


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

	chest = malloc(sizeof(Chest));
	memset(chest, 0, sizeof(Chest));
	chest->item = item;

	removeEntityFromDungeon(item);

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

	e->touch = touch;
}

The initChest function has also been updated to take an entity as the item contained within. We've made some assumptions here (and throughout the tutorial) as to this item. Most importantly, it will only ever be one item and the item itself is assumed not to be NULL.

The first thing we do in the function now is malloc a Chest object. We then assign the item we passed into the initChest function to the Chest. With that done, we remove the item from the dungeon, with a call to removeEntityFromDungeon. The reason for this is because own spawnEntity function always adds entities to the dungeon when they are created. We could've made a new function to not do this or tweak the spawnEntity function to made the adding optional, but there's no harm in leaving it as is, since most entities will be added to the dungeon anyway.

The final change is to assign the created Object to the chest entity's data field.

We've made some big changes to the touch function, and this is where the bulk of our interaction logic will lie. Previously, the chest would simply respond to a touch call with a message saying it is locked. Now, the chest will interrogate the player's inventory to see if they have a key. Take a look at the function below, and then we'll run through it:


static void touch(Entity *self, Entity *other)
{
	Chest *chest;
	Entity *e;
	char message[64];

	if (other == player)
	{
		chest = (Chest*) self->data;

		if (!chest->isOpen)
		{
			if (hasInventoryItem("Rusty key"))
			{
				chest->isOpen = 1;

				self->texture = getAtlasImage("gfx/entities/openChest.png", 1);

				e = removeInventoryItem("Rusty key");

				e->x = -1;
				e->y = -1;
				e->alive = ALIVE_DEAD;

				addEntityToDungeon(e);

				addToInventory(chest->item);

				sprintf(message, "Got %s", chest->item->name);

				setInfoMessage(message);

				chest->item = NULL;
			}
			else
			{
				setInfoMessage("It's locked and I don't have a key.");
			}
		}
	}
}

As before, the first thing we do is test if the touching entity is the player. If so, we grab the Chest object from the chest's data field. We then test to see if the chest is already open (isOpen). If it is, we don't do anything else. However, if it isn't open, we made a call to a new function called hasInventoryItem, to see if the player has a "Rusty key". We'll see more on this function when we come to inventory.c. For now, know that this function will return 0 (false) or 1 (true) if the named entity exists in the player's inventory.

If we don't have a Rusty key, the old "It's locked ..." message will be displayed. If we do have the key, we move onto the phase of fetching the item from the chest. The first thing we do is set the Chest's isOpen flag to 1, and then update the chest's texture. As the chest is now open, we'll draw it using a different texture to when its closed. This is just a case of calling getAtlasImage. We've decided in our game that keys may only be used once, and so we remove the Rusty key from the player's inventory. The removeInventoryItem will return the removed entity, allowing us to manipulate it further. In this case, we want to destroy it. We do this by setting its alive state to ALIVE_DEAD and adding it back into the dungeon, so that the doEntities loop can destroy it correctly (the doEntities loop ensure that all the data is freed as expected).

With the chest open and the key removed, the only thing left to do is fetch the item from the chest. For this, we call addToInventory, passing over the Chest's item. We don't need to perform a check if it can be added to the inventory, since we already know there is a free slot, having removed the key we were holding. We then set an information message to say that the item has been retrieved, and finally set the chest's item to NULL, effectively marking the Chest as empty.

That's our updates to chest.c done. We can now look at the updates that we have done with inventory.c, to support the interaction with the chest. As we already saw, we added in two new functions. We'll start by looking at hasInventoryItem:


int hasInventoryItem(char *name)
{
	Prisoner *p;
	int i;

	p = (Prisoner*) player->data;

	for (i = 0 ; i < NUM_INVENTORY_SLOTS ; i++)
	{
		if (p->inventorySlots[i] != NULL && strcmp(p->inventorySlots[i]->name, name) == 0)
		{
			return 1;
		}
	}

	return 0;
}

It should be fairly clear what's going on here. We're grabbing the Prisoner data from the player entity, and then looping through the inventory slots to look for an entity with the name of the argument we've passed in. Should we find a match, we'll return 1 (true). If we reach the end of our inventory list and find nothing, we'll return 0 (false).

The other function we added is removeInventoryItem. There won't be too many surprises here:


Entity *removeInventoryItem(char *name)
{
	Prisoner *p;
	Entity *e;
	int i;

	p = (Prisoner*) player->data;

	for (i = 0 ; i < NUM_INVENTORY_SLOTS ; i++)
	{
		if (p->inventorySlots[i] != NULL && strcmp(p->inventorySlots[i]->name, name) == 0)
		{
			e = p->inventorySlots[i];

			p->inventorySlots[i] = NULL;

			return e;
		}
	}

	return NULL;
}

Again, we're looping through the player's inventory, searching for the named item. When we find it, we're grabbing a reference to it, NULLing the inventory slot to effectively mark it as unused, and then returning the entity that formerly occupied the slot. If we find nothing, we're returning NULL. Under different circumstance, we'd need to be careful that the item we removed isn't NULL. However, in our case we already checked to see if it existed before removing it, so we're safe.

We're almost done with our update. Let's take a quick look at how we're making use of the new initChest function, done in the initDungeon function:


void initDungeon(void)
{
	Entity *e;

	initMap();

	initEntities();

	initPlayer();

	initHud();

	initInventory();

	initItem("Rusty key", 8, 11, "gfx/entities/rustyKey.png");

	e = initItem("Eyeball", 8, 11, "gfx/entities/eyeball.png");

	initChest(21, 14, e);

	e = initItem("Red potion", 8, 11, "gfx/entities/redPotion.png");

	initChest(24, 17, e);

	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 creating our Rusty key as normal, placing it away from the chests (to give the prisoner some exercise!). We're then calling initItem, to create an eyeball, and grabbing the returned reference to the entity (this is a very minor change that we're not covering here). With item reference in hand, we're calling initChest and passing the eyeball over. We're doing the same thing with the red potion. In effect, we're creating the eyeball and red potion, and placing them inside a chest each.

The final tweak comes in our drawEntities loop, in entities.c:


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

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->alive != ALIVE_DEAD)
		{
			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);
			}
		}
	}
}

We're checking that the entity isn't dead before drawing it. This is because used items, such as the rusty key, are returned to the dungeon for clean up. However, they might be briefly drawn before being removed, depending on the order in which entities are processed. This is just a precaution to prevent undesirable graphics glitches.

So, there we have it - the ability to manage an inventory and use it to interact with other entities in the dungeon. Our game is starting to take shape. A few more updates and tweaks, and we'll be ready to implement the game proper. At this point, something that would make the dungeon more interesting is a fog of war effect, meaning that the map will be hidden in darkness until we start to explore it. This will grant a lot of mystery to the proceedings. We'll take that in the next part.

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

If you do not wish to create an itch.io account, you can also purchase the tutorial bundle using PayPal. This method will be slower, however, as it will require manual verification of the transaction.

Comments

Share your comments and thoughts below. All comments are anonymous and cannot be edited.

 

Mobile site