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 12: Vampire Bats!

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

Introduction

The third icon that the Prisoner needs to find is guarded by a hoard of vampire bats! In order to get past them, the Prisoner will need to get his hands on a silver dagger. This can be done by fetching 14 silver coins and giving them to the blacksmith, who will furnish the Prisoner with the weapon.

Extract the archive, run make, and then use ./adventure12 to run the code. The usual controls apply. The 14 silver coins can be found scattered around the dungeon, so you will need to explore. Once you find all of them, return to the blacksmith, and then, with the dagger in your inventory, make your way to the vampire bats (Casa de Chiroptera). To kill the bats, simply walk into them. You will soon get the icon. Close the window to exit.

Inspecting the code

We've added in our final NPC: a blacksmith. He resides in the same area as the Dungeon Mistress and the Merchant, in the starting zone. He doesn't operate that much different from the Merchant. Let's start by looking at structs.h:


typedef struct {
	int state;
	int itemId;
	Entity *item;
} Blacksmith;

Our Blacksmith struct is basically identical to ther Merchant. There was a temptation here to change both the Merchant and Blacksmith into a single struct called ShopKeeper, but doing such things could lead to problems when creating larger projects, so best to leave them as separate structures (unless we suddenly found we had half a dozen identical ones..!).

Moving on, the Blacksmith's functions are all defined in blacksmith.c. There are three functions here - the init, the touch, and the load. We'll start with initBlacksmith:


void initBlacksmith(Entity *e)
{
	Blacksmith *blacksmith;
	blacksmith = malloc(sizeof(Blacksmith));
	memset(blacksmith, 0, sizeof(Blacksmith));

	STRCPY(e->name, "Blacksmith");
	e->texture = getAtlasImage("gfx/entities/blacksmith.png", 1);
	e->solid = SOLID_SOLID;
	e->data = blacksmith;

	e->touch = touch;
	e->load = load;

	mbColor.r = 64;
	mbColor.g = 16;
	mbColor.b = 64;
}

You'll know what's going on here by now; it's the usual data setup for the blacksmith, as with the other NPCs. We can quickly move on to the touch function:


static void touch(Entity *self, Entity *other)
{
	Blacksmith *b;
	Prisoner *p;
	char message[128];

	if (other == player)
	{
		self->facing = (other->x > self->x) ? FACING_RIGHT : FACING_LEFT;

		b = (Blacksmith*) self->data;
		p = (Prisoner*) other->data;

		switch (b->state)
		{
			case STATE_INIT:
				addMessageBox("Blacksmith", "Alright, mate? The devil in the blue dress got you playing her little game, has she? You're not going to have much luck against the vampire bats using just your bare hands.", mbColor.r, mbColor.g, mbColor.b);
				addMessageBox("Prisoner", "I was thinking the same. Got a magical sword or something you can sell me?", p->mbColor.r, p->mbColor.g, p->mbColor.b);

				memset(message, 0, sizeof(message));
				sprintf(message, "I do! All I'll need is %d silver coins. You'll find plenty lying around the dungeon, I'm sure.", SILVER_REQUIRED);

				addMessageBox("Blacksmith", message, mbColor.r, mbColor.g, mbColor.b);
				addMessageBox("Prisoner", "Ha, I see. You'd melt down the silver into the blade of a magical weapon, to use against the bats?", p->mbColor.r, p->mbColor.g, p->mbColor.b);
				addMessageBox("Blacksmith", "What? No! Those aren't real silver. They're just chocolate, wrapped in silver paper.", mbColor.r, mbColor.g, mbColor.b);
				addMessageBox("Prisoner", "...", p->mbColor.r, p->mbColor.g, p->mbColor.b);
				addMessageBox("Prisoner", "What about gold?", p->mbColor.r, p->mbColor.g, p->mbColor.b);
				addMessageBox("Blacksmith", "Got plenty of that, mate.", mbColor.r, mbColor.g, mbColor.b);

				b->state = STATE_NEED_SILVER;
				break;

			case STATE_NEED_SILVER:
				if (p->silver == 0)
				{
					addMessageBox("Blacksmith", "Let me know when you find all the silver.", mbColor.r, mbColor.g, mbColor.b);
				}
				else if (p->silver < SILVER_REQUIRED)
				{
					memset(message, 0, sizeof(message));
					sprintf(message, "Sorry, pal, still short %d. Gonna need all %d.", SILVER_REQUIRED - p->silver, SILVER_REQUIRED);

					addMessageBox("Prisoner", "I managed to find some.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Blacksmith", message, mbColor.r, mbColor.g, mbColor.b);
				}
				else
				{
					if (addToInventory(b->item))
					{
						addMessageBox("Prisoner", "Phew, I think that's all you need?", p->mbColor.r, p->mbColor.g, p->mbColor.b);
						addMessageBox("Blacksmith", "Get in! You legend! Here's the dagger, as promised. You know how to use it?", mbColor.r, mbColor.g, mbColor.b);
						addMessageBox("Prisoner", "You stick 'em with the pointy end.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
						addMessageBox("Blacksmith", "That's the spirit! Best of luck, mate.", mbColor.r, mbColor.g, mbColor.b);

						p->silver -= SILVER_REQUIRED;

						b->item = NULL;

						b->state = STATE_HAS_SILVER;
					}
					else
					{
						addMessageBox("Prisoner", "I can't carry anything else. I'll need to drop something before I can get the dagger.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					}
				}
				break;

			case STATE_HAS_SILVER:
				addMessageBox("Blacksmith", "Nom nom nom! These coins are really good. I love dark chocolate. None of that milk chocolate stuff for me.", mbColor.r, mbColor.g, mbColor.b);
				break;
		}
	}
}

It might look like there's a lot going on, but keep in mind a lot of this is just text boxes for conversations. When touched, the Blacksmith will face the player, and then test his state. He starts in STATE_INIT, where he'll just chat with the player. After that, he moves into STATE_NEED_SILVER. If the player has yet to find any silver, he will tell them to return once they do. If they have fewer than the required number, he'll inform them they need to find more coins. Otherwise, we can move into the phase of turning over the dagger and completing the task.

Something that's important here is that we're first testing the outcome of addToInventory for the dagger. If we can successfully add the dagger to the player's inventory, we remove the silver, null the Blacksmith's item, and update the Blacksmith's state. This will effectively complete the task. However, if we're unable to add the dagger to the player's inventory, we'll show a message box to say we need to drop something first. This is important, as unlike taking items from a chest or exchanging the record with the Merchant, we cannot be certain an inventory slot will be available. Silver isn't an inventory item, but a separate stat, meaning the player's inventory might be full.

Finally, if the Blacksmith's state is STATE_HAS_SILVER, he'll merely say how much he's enjoying the chocolate we've brought him. The final function to look at is load. It's not much different from the Merchant or Chest:


static void load(Entity *e, cJSON *root)
{
	Blacksmith *blacksmith;

	blacksmith = (Blacksmith*) e->data;

	blacksmith->itemId = cJSON_GetObjectItem(root, "itemId")->valueint;
}

We're just looking for the item to set to the Blacksmith, according to the item id. This item id is used in the same way as the Chest and Merchant in the postLoad function, in entities.c:


static void postLoad(void)
{
	Entity *e;
	Chest *chest;
	Merchant *merchant;
	Blacksmith *blacksmith;

	for (e = dungeon.entityHead.next ; e != NULL ; e = e->next)
	{
		if (strcmp(e->name, "Chest") == 0)
		{
			chest = (Chest*) e->data;

			chest->item = getEntityById(chest->itemId);

			removeEntityFromDungeon(chest->item);
		}
		else if (strcmp(e->name, "Merchant") == 0)
		{
			merchant = (Merchant*) e->data;

			merchant->item = getEntityById(merchant->itemId);

			removeEntityFromDungeon(merchant->item);
		}
		else if (strcmp(e->name, "Blacksmith") == 0)
		{
			blacksmith = (Blacksmith*) e->data;

			blacksmith->item = getEntityById(blacksmith->itemId);

			removeEntityFromDungeon(blacksmith->item);
		}
	}
}

We're doing the same thing for each data structure here. Should this function have started to grow larger, we might want to create a postLoad function pointer in the Entity structure, and call that instead of using this global system that tests the name of the entity.

That's the Blacksmith handled, so we can see what else has now changed. As stated earlier, we've updated the Prisoner so that he can hold silver, as well as gold and have an inventory. However, we've also added another new field:


typedef struct {
	int gold;
	int silver;
	Entity *inventorySlots[NUM_INVENTORY_SLOTS];
	int hasLantern;
	int hasDagger;
	SDL_Color mbColor;
} Prisoner;

hasDagger will be used to determine whether the player is holding the dagger, much in the same way that we test if the player has the Lantern. We've updated the updatePrisonerAttributes function in inventory.c to deal with this:


static void updatePrisonerAttributes(Prisoner *p)
{
	int i;

	p->hasLantern = p->hasDagger = 0;

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

			if (strcmp(p->inventorySlots[i]->name, "Dagger") == 0)
			{
				p->hasDagger = 1;
			}
		}
	}
}

A simple update. We've just checking to see if the player is holding an inventory item called Dagger, and setting the hasDagger field to 1 (true) if they do.

As one of the objectives of the adventure is to find silver coins, we need to define these. Remember that silver coins are not items, and they are not gold coins either, so need different logic. This is defined in silver.c. The file is simple, containing just two functions - an init and a touch. The initSilver function is simple:


void initSilver(Entity *e)
{
	e->texture = getAtlasImage("gfx/entities/silverCoin.png", 1);

	e->touch = touch;
}

We're setting the texture of our entity and assigning the touch function. Nothing we've not seen before. The touch function itself is also very simple:


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

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

		p->silver++;

		self->alive = ALIVE_DEAD;

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

When the Prisoner touches a silver coin, their silver variable is incremented, the coin is marked as dead, and we set a message saying we've picked up a coin.

But what of the vampire bats themselves. Well, they're also very simple in their implementation. Defined in bat.c, they have just two functions - an init and a touch. Starting with initBat:


void initBat(Entity *e)
{
	e->texture = getAtlasImage("gfx/entities/vampireBat.png", 1);
	e->solid = SOLID_SOLID;

	e->touch = touch;
}

There's probably not much to say about this, to be honest. The bats are solid, though, so that they block the player movement (and line of sight). The touch function is a little more interesting:


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

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

		if (p->hasDagger)
		{
			self->alive = ALIVE_DEAD;
		}
		else
		{
			addMessageBox("Prisoner", "Ouch! It bit me. I can't get past.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
		}
	}
}

This is where the Prisoner's hasDagger variable comes into play. When the player walks into a bat, we test to see if the player has the dagger. If so, the bat is killed by setting its alive state to ALIVE_DEAD. However, if the player doesn't have the dagger, the Prisoner will complain the bat bit him. As the bat is solid, it becomes impassable and prevents the player from proceeding any further.

That's more or less it for the Blacksmith, coins, and bats. To enable them in our dungeon, we need to remember to add them to initEntityFactory:


void initEntityFactory(void)
{
	memset(&initFuncHead, 0, sizeof(InitFunc));
	initFuncTail = &initFuncHead;

	addInitFunc("player", initPlayer);
	addInitFunc("item", initItem);
	addInitFunc("chest", initChest);
	addInitFunc("gold", initGold);
	addInitFunc("silver", initSilver);
	addInitFunc("signpost", initSignpost);
	addInitFunc("torch", initTorch);
	addInitFunc("goblin", initGoblin);
	addInitFunc("door", initDoor);
	addInitFunc("dungeonMistress", initDungeonMistress);
	addInitFunc("merchant", initMerchant);
	addInitFunc("blacksmith", initBlacksmith);
	addInitFunc("bat", initBat);
}

Before we finish up, let's take a quick look at the other changes we've made. As we can now collect 3 icons, we've updated the Dungeon Mistress to respond to this event. In dungeonMistress.c, the touch function now handles 3 Icons having been returned:


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

	if (other == player)
	{
		self->facing = (other->x > self->x) ? FACING_RIGHT : FACING_LEFT;

		d = (DungeonMistress*) self->data;
		p = (Prisoner*) other->data;

		if (hasInventoryItem("Icon"))
		{
			removeInventoryItem("Icon");

			d->iconsFound++;

			switch (d->iconsFound)
			{
				case 1:
					addMessageBox("Prisoner", "I got one of the icons!", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "You found one? Beginner's luck, I guess. Well, don't expect the others to come so easily. I'll just take that from you ...", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 2:
					addMessageBox("Prisoner", "Here you go ...", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "Another one? No, you're cheating. This has got to be a fake. I'll have it checked ...", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 3:
					addMessageBox("Prisoner", "another one.gif", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					addMessageBox("Dungeon Mistress", "What the flip?! Stop looking up the answers on the internet!", mbColor.r, mbColor.g, mbColor.b);
					addMessageBox("Prisoner", "See you again in a bit.", p->mbColor.r, p->mbColor.g, p->mbColor.b);
					break;

				default:
					break;
			}
		}
		else
		{
			switch (d->iconsFound)
			{
				case 0:
					addMessageBox("Dungeon Mistress", "Not found any yet? Aw, poor baby. Going to be here at while, aren't you? Heh heh heh!", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 1:
					addMessageBox("Dungeon Mistress", "Don't get excited, hon. You've only found one icon so far.", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 2:
					addMessageBox("Dungeon Mistress", "Halfway there, but you'll never find the rest. You'll starve to death down here. Ha ha ha!", mbColor.r, mbColor.g, mbColor.b);
					break;

				case 3:
					addMessageBox("Dungeon Mistress", "I've had the WiFi password changed, so you can't keep cheating. You're not going to break my winning streak.", mbColor.r, mbColor.g, mbColor.b);
					break;

				default:
					break;
			}
		}
	}
}

The Dungeon Mistress is starting to become annoyed with the Prisoner's progress, finding it unbelievable that he's managing to find the Icons, and accusing him of cheating when he turns over the 3rd one.

The final little tweak we've made is the initPlayer, to set the colour of his message box:


void initPlayer(Entity *e)
{
	Prisoner *p;

	player = e;

	p = malloc(sizeof(Prisoner));
	memset(p, 0, sizeof(Prisoner));
	p->mbColor.r = p->mbColor.g = p->mbColor.b = 32;

	player->solid = SOLID_SOLID;
	player->texture = getAtlasImage("gfx/entities/prisoner.png", 1);
	player->facing = FACING_LEFT;

	player->data = p;

	movePlayer(0, 0);

	moveDelay = 0;
}

Along with the new silver and hasDagger variables that we added to the Prisoner struct, we also added in an SDL_Color called mbColor (message box colour). This can be used when the player is speaking, so that can centralize the colour of the Prisoner's message box. If we want to change the colour, we need just update this variable, rather than hunt around for each place he speaks to update those.

We're almost finished! Just one room left to create, and then some finishing touches to go! In the next tutorial, we'll look at creating the Escape Room, a room that shuts the Prisoner in and seemingly has no way out.

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