• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Simple 2D adventure game —
We can now add the second magical Icon to find. This one will be located in the so-called Cursed Maze, an area of the game that doesn't light up normally and will require a lantern. Two new characters have been added to the start area: the Merchant and the Dungeon Mistress. The Merchant will sell the player the lantern, in exchange for another item, while the haughty Dungeon Mistress will be the one to whom the player returns the Icons they've found.
Extract the archive, run make, and then use ./adventure11 to run the code. The usual controls apply. The goal is to fetch the Envelope Icon from the Cursed Maze. Two keys now exist in the maze, along with two chests. One of the chests contains the item the Merchant desires. Fetch the item and take it to the Merchant, then make your way to the Cursed Maze. Navigate the maze to find the Icon, and then return it to the Dungeon Mistress. Close the window to exit.
Inspecting the code
As already stated, we've added into two new characters, plus an area where our normal fog of war rules don't apply. We'll start by looking at the Merchant. He operates much the same way as the Goblin does, and he has his own definition in structs.h:
Just like the Goblin, we've got a variable to hold the Merchant's state, which will be used to control how he responds to the player. We've also added in an itemId and item entity pointer. You'll likely recognise this as the same way the Chest works. The Merchant will hold the item he will exchange with you. The Merchant himself is defined in merchant.c. We'll start, as always, with the init function:
Nothing we've not seen before. We're mallocing the Merchant data, setting the name, texture, solid state, and data, and the touch and load function pointers. We're also setting an SDL_Color variable called mbColor (message box colour) to a light blue. This will be the colour of the merchant's message box.
The touch function is where the Merchant's logic lives. If you've read the Goblin tutorial, it should look quite familiar:
When the player touches the Merchant, the Merchant will face the character, and then react according to his state. In STATE_INIT, a conversation will be had, and the Merchant will then move into STATE_WANT_ALBUM. In STATE_WANT_ALBUM, we'll check to see if the player has Rumours item in their inventory. If so, another conversation will be had, Rumours will be removed from the player's inventory, and the Merchant's item will be placed in the player's inventory. We'll null the Merchant's item and then set the Merchant's state to STATE_HAS_ALBUM. Nulling the Merchant's item has no real effect, as it is never checked in our logic. It's good practice. If Rumours doesn't exist in the player's inventory, the Merchant will prompt the player to find it. Finally, in the STATE_HAS_ALBUM state, the Merchant will respond with a single message.
A lot of talking, and some item exchange logic. Pretty simple. The Merchant's load function is equally simple:
It's basically the same as the Chest, except that we're using a Merchant instead of a Chest.
We'll stick with the game flow around the Merchant for now, and look at the updates made to the Prison and the fog of war. In order to work with darkness of the Cursed Maze, we need to make some logic changes. Starting with the Prisoner struct:
We've added in a variable called hasLantern. By default, this will be 0, indicating that we don't have a lantern in our inventory. The reason we're doing this will become clear in a moment. For now, let's look at how we set this variable. Turning to inventory.c, we've made a change to addToInventory:
We've added a call to a new function called updatePrisonerAttributes whenever we pick up an item, defined before:
The idea behind this function is to test the items that exist in our inventory. By default, we're setting the Prisoner's hasLantern variable to 0. We're then looping through all the items in the inventory to see if the Lantern exists. If so, we're setting the hasLantern variable to 1. This same function is called by dropSelectedItem:
If we drop the Lantern, we want to make sure that we can no longer navigate the Cursed Maze, as we don't have the required item in our inventory. The reason we're setting a variable like this is so that each time we move, we don't loop through our inventory and test each object to see if it's the Lantern. Basically, we're just saving some execution time.
Now let's look at how we're dealing with the Cursed Maze. If you've entered it without the Lantern, you'll notice that it remains completely dark. The first thing we've done is update defs.h:
TILE_DARK is a new define. What we're saying here is that any tile index between 35 and 39 (inclusive) will be a dark tile. Our map data for the Cursed Maze has all its tiles (other than the walls) declared as index 35.
If we now look at fogOfWar.c, we can see how this is being applied in the hasLOS function:
The first thing to note is that we've changed the function parameters. Before, it took x1, y1, x2, y2 as it's arguments. Now it's taking the source entity, x2, y2. The purpose of this is so that we can test is the entity in question is the Prisoner, to test the presence of the Lantern. Before entering our while loop, we're now testing to see if the tile at x1, y1 falls within the dark tile range, if so, and the entity passed into the function is not the player, or it is the player and they don't have the lantern, we immediately return 0, to say that there is no line of sight. What this means is that our line of sight is blocked instantly if standing on a dark tile.
We repeat this check during our while loop, for each tile we come to during our line of sight test. This is important to ensure that we correctly block the line of sight, and that some of tiles are lit by mistake (such as the entrance to the maze).
That's all we need to do to block the line of sight - use a different set of tile indexes and test them during our fog of war update. We've also decorated the map's dark tiles, in the same way we do with the regular ground tiles:
We're nearly done with this part of the update. Let's now look at the Dungeon Mistress. Starting with structs.h, we can see she's quite simple:
She merely has a variable to hold the number if icons that the player has found. We can see this in use in dungeonMistress.c. Before we get to that, we'll quickly look at the init function:
Just like the Goblin and Merchant, we're setting up the Dungeon Mistress's various pieces of entity data, touch, and message box. And once again, the touch function is where the main logic happens:
When the player walks into the Dungeon Mistress, she will turn to face them, and then check to see if the player is carrying an Icon. If so, the Icon is removed from the player's inventory and the Dungeon Mistress's iconsFound variable is incremented by 1. A conversation will then be displayed, based on the number of icons that have been found so far. If the player isn't carrying any Icons, the Dungeon Mistress will taunt the player with various other messages. Returning all the icons to the Dungeon Mistress is what will ultimately allow the prisoner to escape the dungeon.
We've now completed half of our little adventure game! Hurrah! There are just two more icons left to find, and two other challenges remaining to create. In the next part, we'll look at dealing with Vampire Bats, and introduce the Blacksmith character, as well as some other items to find and use.
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.
Share your comments and thoughts below. All comments are anonymous and cannot be edited.