• 2D shoot 'em up
SDL2 turn-based strategy tutorial
Water Closet ported to PlayStation Vita
The Legend of Edgar 1.35
SDL2 Rogue tutorial
— Creating a simple roguelike —
In this part, we're finally going to be making use of those keys we've been finding lying around. To do this, we're going to insert some doors into the dungeon, some of which will be open, others locked. The locked doors will require the player to have a key in their inventory to open. We're also going to be shifting items such as weapons, armour, and microchips, to alcoves into the dungeon, rather than have them sitting out in the middle of the floor.
Extract the archive, run make, and then use ./rogue14 to run the code. You will see a window open displaying the player character in a small room, with just a stair case (leading up). Play the game as normal. If you come across a door, a red light at the bottom will indicate that it requires a key to open it. Walk into it to use the key (which will be lost). Doors with a green light do not require a key to open and can be walked into normally. Once you're finished, close the window to exit.
Inspecting the code
Adding the doors to our dungeon is a bit more complicated than one might think, due to the need to identify where they should be placed. We'll see how this is done when we come to creating them.
To begin with, let's look at the changes we've made to defs.h:
We've added in a new ET_ type - ET_DOOR will identify the entity as a door.
Turning next to structs.h, we've added in a new struct:
We've created a Door struct, with a single field - `locked`. `locked` will specify whether the door is locked (1) or unlocked (0). Easy.
To handle our doors, we've added in a file called doors.c. As always, there are plenty of functions to get through, though the coding pattern should be quite familiar by now. Starting with initDoor:
As we've seen a few times before, this is a helper function for creating Doors. It handles all the common data setup. The function takes two parameters - the Entity (`e`) and an int called `locked`. `locked` will tell us whether the Door we create is locked. We start by mallocing and memsetting a Door (as `d`), and setting the value of its `locked` field to the value of `locked` we passed into the function. Folowing that, we set `e`'s type to ET_DOOR, set its `data` field to `d`, and also set `e`'s `solid` and alwaysVisbile values to 1. Like with stairs, we don't want our doors to vanish from sight if the player's LOS is obscured. Finally, we set `e`'s `touch` to the `touch` function in doors.h and return `e`.
If we now look at initDoorNormal, we can see how we're using initDoor:
initDoorNormal is an init function that creates an unlocked door. We're setting `e`'s `name`, `description`, and `texture`, and then calling initDoor, passing over `e` and 0, to say that the door is unlocked.
If we look now at initDoorLocked, we can see we're doing largely the same:
Other than the different `name`, `description`, and `texture`, we're passing 1 over to initDoor, to say that this door is locked.
The `touch` function is next and is where things get a bit more interesting:
Like many other entities, we're testing if the thing (`other`) that has touched the door (`self`) is the player. If so, we're extracting the Door from `self`'s `data` field (assigning to `d`), and then checking the `locked` state. If `d`'s `locked` is 0, we're going to set `self`'s `dead` flag to 1, to remove it from the dungeon. If the door is locked, however, we're going to do some other things.
First, we're going to call a new function named getInventoryItem and pass over "Key", and assign the result to an entity variable called `key`. This function will basically search our inventory for an item called "Key". If `key` is not NULL (we found a Key in our inventory), we'll call removeFromInventory and pass `key` over. Next, we'll add `key` to our dungeon's dead list, to remove it fully from our dungeon (and also ensure the data is fully removed), set `d`'s `locked` state to 0, and add a HUD message to say the door has been unlocked. So, in short, we'll check if we have a key in our inventory and use it to open the door.
If, however, we don't have a key, we'll add a HUD message to say that the door is locked and a key is required. We'll also call a new function named clearInput. What this function does is clear all the current input and cancels our keyboard and mouse controls. We're doing this so that a player doesn't push against the door and fill their HUD with messages that the door is locked. While they can still tap the keyboard or mouse to achieve the same, this does help to mitigate the issue.
Moving on, we have a function called addDoors:
It's fairly long, as you can see. What this function does is search the map for alcoves (floor tiles that are surrounded by at least 7 walls) and the follows the route out into the open (when it finds a ground square surrounded by fewer than 6 walls). Once there, it places a door. In short, it places a door at the entrance to a passageway that ends in a deadend (which might be a location where we've placed an item - we'll come to that a bit later on).
To start with, we setup two for-loops, to traverse the map. Note that we're keeping 1 square away from the edges of the map. We're testing each tile that we come to, to see if it's a ground tile, and also whether it is surrounded by more than 7 walls (using countWalls). If so, we've found an alcove in the map. We'll set a variable called `steps` to 0 and variable called `dx` to 1 and a variable called `dy` to 0. We'll also assign two variables called `mx` and `my` to the current values of `x` and `y`, respectively.
We're then setting up a do-loop, to test the square at `mx` + `dx` and `my` + `dy`, to see if it's a ground tile. If so, we'll add `dx` and `dy` to `mx` and `my`, and then increment `steps`. If it's not a ground tile, we'll be rotating through 90 degrees clockwise, to try a different direction (you'll have to trust me on this one that this is what the code is doing, since this is essentially a 2D matrix transform ..!). We'll continue this do-loop while the call to countWalls at `mx` and `my` is greater than 6. A count of 6 means we're still in the corridor, and will continue to follow it until we exit. This allows us to follow straight corridors, as well as ones that twist and turn (through 90 degrees).
Upon exiting, we'll test the value of `steps`, to find out how long our corridor is. If it's greater than 3, we'll add a door. Using rand, there's a one in three chance that the door will be locked. Otherwise, we'll add in normal (unlocked door). We'll call the relevant init function and assign the result to `e`. We'll then set `e`'s `x` and `y` values to those of `mx` and `my` (which will be the end of the corridor).
We're now able to add in doors at the entrances to corridors, that can potentially seal off access to items that the player might find useful, unless they have a key with them. We'll see shortly how we add our items to these alcoves.
Next, let's look at the changes to inventory.c, where we've added in a new function - getInventoryItem:
This function is quite straightforward - we just loop through our inventory, searching for an entity with a `name` that matches the one passed into the function. Once we find one, we return it. Otherwise, the function will return NULL.
Moving over to items.c, we've updated our addItems function:
To begin with, we're now adding between 0 and 2 keys or health packs to the dungeon, anywhere about the floor. Next, we're looping through all our map tiles, in the same way as we did when adding in the doors, searching for alcoves (a floor tile with 7 or more walls surrounding it). Upon finding one, there's a 2 in 3 chance that we'll call a function named createRandomItem at the current position (`x` and `y`). We'll assign the result of call (an Entity) to `e` and then set `e`'s `x` and `y` to the values of `x` and `y` in the loop. Basically, we're looking for alcoves and then adding random items to them.
createRandomItem is a simple function:
We're just performing a switch against a random of 6. Based on the result, we're calling initEntity for a named item and returning it. Little more needs saying about the function. Note that we'll never hit NULL due to all our cases being handled; we're just doing so to silence the compiler warnings.
We're almost done with our changes. We just have some simple updates to make. Turning to entities.c, we've updated isBlocked:
We've added ET_DOOR to our switch statement. Like with some of the other entities, we're calling the door's `touch` function. Notice, however, that we're returning 1 to say that the door is blocking our movement. For a locked door, we'll not proceed any further. For an unlocked door, this will result in the door disappearing before we can then proceed.
Next, we turn to dungeon.c, to update createDungeon:
We've added in a call to addDoors. Something we've also done is taken away the calls to addWeapons, addArmour, and addMicrochips. This is because these functions have now been removed from the source code, due to addItems now handling everything.
And, of course, the last thing we need to do is update entityFactory.c, to add in our door init functions. Two new lines in initEntityFactory is all that's required:
Our doors can now be created as needed!
Another part down, but we still have several to go. Building roguelikes can take a while, as one can see. Our next part is going to focus on the one thing that we have been ignoring up until now - handling the player's death. No longer will our brave tech be able to swan around with -22012 hit points. We'll also throw in a highscore table, to keep track of each attempt to slay the Mouse King!
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle:
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.