• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a simple roguelike —
We're now able to pick up items and use them, but what about things like weapons? In this part, we'll be introducing weapons, armour, and a microchip (as a bonus piece of equipment). Again, there are no monsters to be found and we've taken the health packs and keys away, to focus on the equipment. In this dungeon, you'll find a crowbar, a biker jacket, and a microchip.
Extract the archive, run make, and then use ./rogue09 to run the code. You will see a window open like the one above, showing our main character in a dungeon environment. Use the same controls as before to move around and open the inventory. Pick up the items as you find them. To equip an item, highlight it in the inventory and click Use. To remove it, highlight it in the equipment section and click Remove. This will return the item to the inventory. Notice how your stats change as you equip and remove items. Once you're finished, close the window to exit.
Inspecting the code
Like the previous part, this part is quite long, so we'll not linger. Starting with defs.h:
We've added in three new types to our ET enum. ET_WEAPON will define a weapon entity type, ET_ARMOUR an armour entity type, and ET_MICROCHIP a microchip entity type.
We've also added in a brand new entity type called EQUIP:
These will describe the type of equipment an item is. Our equipment slots are a fixed array size, allowing for three things to be set (but only one of each).
Now, let's move over to structs.h, where we've also made some updates and additions:
The new Equipment struct represents a piece of equipment that the player can use. Each field represents a value by which the player's attributes would be modified. `hp` modifies the player's maxiumum hit points, minAttack their minimum attack value, maxAttack their maximum attack value, and `defence` their defence value. Note that these values can negative as well as positive.
Next, we've modified Game:
We've added in an array of Entity pointers called `equipment`, into which our equipped items will live, of EQUIP_MAX size.
Now, let's move over to armour.c, a new file, which is where we're defining our armour. Starting with addArmour:
This function is merely responsible for adding a piece of armour to the dungeon. We just calling addEntityToDungeon and passing over the entity that was created by the call to initEntity. Here, we're asking the game to add in a Biker Jacket.
Next, we have createArmour:
This is a helper function, quite a lot like createItem and createMonster. The function takes a single argument - an Entity (`e`). We're first mallocing and memsetting a piece of Equipment (as `eq`), then setting the Entity's type to ET_ARMOUR and its `data` field as the Equipment we created. We're then assigning the entity's `touch` function and returning the Equipment.
Next is initBikerJacket:
This is the init function for creating a Biker Jacket. We're calling createArmour and assigning it to `eq`. We're then setting the entity's `name`, `description`, and `texture`, and finally setting the Equipment's `defence` to 5. This means that our Biker Jacket will add 5 to the player's `defence` when it is equipped.
Finally, we have the `touch` function:
Like the `touch` function for our items, we're testing if the thing that has walked into the Equipment is the player, before adding it our inventory and displaying a message.
We've also added in a file for handling our weapons, called weapons.c. This is quite similar to armour.c in many aspects, so we'll keep this fairly brief. Starting with addWeapons:
We're just adding an entity called Crowbar to the dungeon.
Next is createWeapon:
This is much like createArmour, except that the entity's type is ET_WEAPON.
Again, much like initBikerJacket. However, since this is a weapon we're affecting the player's attack stats. We're setting the Equipment's minAttack to 3 and maxAttack to 6. This means that, when equipped, our Crowbar will add 3 to the player's minAttack and 6 to their maxAttack.
The `touch` function is identical to the one in armour.c:
This function could be made global since it's the same for both items, weapons, and armour. For this tutorial, we'll leave it as is.
We also have microchips.c, containing several short functions. Starting with addMicrochips:
We're just adding a Microchip to the dungeon.
createMicrochip throws up no surprises:
We're creating a piece of Equipment and setting the Entity's type to ET_MICROCHIP.
initMicrochip, however, is more interesting:
After calling createMicrochip and setting the entity's `name`, `description`, and `texture`, we're randomly setting the microchip's stats. Since a microchip can affect any of the player's stats, we're randomly setting `hp`, minAttack, maxAttack, and `defence` to values between -1 and 1. We're doing this in a do-loop, so that we can repeat the process in case all values are 0. After setting the values of the fields, we're adding them up and assigning the result to a variable called `points`. We're going to make the loop repeat while `points` is 0 (in other words, while the chip has no attributes!). So, our chip could, for example, add 1 to the player's `maxHP`, but subtract 1 from `defence`. We'll be tweaking this in a future part to make it more interesting.
The `touch` function come last:
It's the same as all the others, so nothing more to add.
Now, turning to inventory.c, this is where we'll handle our equiping our items. We've made quite a few things, some minor, others major.
Starting with initInventoryView:
selectedEquipmentSlot is static int within inventory.c to say which equipment slot we currently have selected. It could be a number from 0 to EQUIP_MAX (exclusive).
Next, we've made an update to `logic`:
After doInventorySlots, we're now calling doEquipmentSlots. We'll see more on this in a bit.
First up, we'll look at the changes we've made to doInventorySlots:
After our for-loop, we've added in a new piece of logic, to test to see if we have an inventory item selected, by testing if selectedInventoryItem isn't NULL. If it's not, we're going to perform a switch on its `type`. Depending on the type of entity (ET_WEAPON, ET_ARMOUR, or ET_MICROCHIP), we'll be setting the value of selectedEquipmentSlot (to either EQUIP_WEAPON, EQUIP_ARMOUR, or EQUIP_MICROCHIP). The idea behind this is to enforce the equipment slot that can be used for a piece of armour, a weapon, or a microchip. We're not going to allow a player to set the microchip into the weapon slot, for example.
We now come to doEquipmentSlots:
This function is responsible for handling when the player clicks on an equipment slot. We're setting up for a for-loop from 0 to EQUIP_MAX, and then assigning the equipmentBoxes at array index `i` to an SDL_Rect pointer called `r`. Note that equipmentBoxes is a static SDL_Rect array in inventory.c. We're then testing to see if the player has placed the mouse pointer over the equipment box, by testing for a collision and feeding in `r`'s attributes, as well as the mouse's. If a collison has occurred, we're checking if the left mouse button is being pressed and clearing it if so. We're then testing whether this equipment box is not already selected by testing if `i` is not selectedEquipmentSlot, and setting selectedEquipmentSlot to `i` if so. Otherwise, we're setting selectedEquipmentSlot to -1, to deselect it. Lastly, we're setting selectedInventoryItem to NULL. Again, this is to enforce our equipment contract - we want to ensure that only armour, weapons, and microchips can be set at certain slots.
Next, we've updated `use`:
We've added to our switch statement, and are now handling ET_WEAPON, ET_ARMOUR, and ET_MICROCHIP. Whenever we click Use on a piece of armour, a weapon, or a microchip, we'll enter this switch case. We'll be setting game's equipment at array index selectedEquipmentSlot to selectedInventoryItem. We're then calling removeFromInventory, passing over selectedInventoryItem, and finally setting selectedInventoryItem to NULL. In effect, this moves the selected inventory item from our inventory to an equipment slot. With that done, we then call updatePlayerAttributes, to update our stats. We'll see more on this a bit later on.
As you can see, equipping an item is quite simple. We have also updated removeItem, to now give it some funtionality:
Removing equipped items is just as easy as equipping them. We first test that selectedEquipmentSlot is not -1 (to ensure we have something selected) and also that the equipped item at game's equipment array index at selectedEquipmentSlot is not NULL, before proceeding. If the item is valid, we'll call addToInventory, passing over the entity at the equipment index, and then set the equipment index to NULL. We'll then set selectedEquipmentSlot to -1 to deselect it, and finally call updatePlayerAttributes, to reflect the changes. In short, we're just moving the item from our equipment array back into our inventory linked list.
That's our equipment logic all handled. We're now left with the rendering. Starting with `draw`:
We've added just one line - drawEquipmentBoxes:
As you can see, this function is quite similar to drawInventoryBoxes. We're first drawing the "EQUIPMENT" text, scaled up by 2. Next, we're setting up a for-loop, from 0 to EQUIP_MAX, and assigning the equipmentBoxes at index `i` to `r`, an SDL_Rect pointer. We're drawing the filled rectangle in as alpha black shape, and then testing the value of selectedEquipmentSlot against `i`. If they're both the same (the box is selected), we'll draw an outline rectangle in yellow. Otherwise, the box will be drawn as a light grey. We're then testing whether the entity at game's equipment array at index `i` is not NULL before then calling drawAtlasImage to draw it.
Finally, we're testing whether selectedEquipmentSlot is not -1 and also whether an entity at selectedEquipmentSlot exists, before drawing its info. We're also testing that selectedInventoryItem is NULL, so that we don't draw both pieces of info at the same time, which would make things hard to read. The selected inventory item will always take priority.
The last change we've made is to setupUI:
Much like how we're creating our array of inventoryBoxes, we're setting up our equipmentBoxes. We're using a for-loop from 0 to EQUIP_MAX, and assigning the equipmentBoxes at index `i` to an SDL_Rect pointer called `r`, before setting `r`'s `x`, `y`, `w`, and `h` fields. `r`'s `x` is 100, plus the value of `i` multiplied by 100, to make each box lie horizontally to the right of the previous one, 100 pixels apart.
That's it for inventory.c! We're now able to equip and remove items!
We should now look at the other changes we've made. Turning to player.c, we've made a change to initPlayer:
Before, we were setting the player Monster's attributes (`hp`, maxHP, etc). We've now mostly replaced this with a call to updatePlayerAttributes. We're then setting the player's Monster `hp` to its maxHP. This is because updatePlayerAttributes updates the value of maxHP but not the value of `hp`. We'll see this in action now:
updatePlayerAttributes is a function to set the player's maxHP, `defence`, minAttack and maxAttack, taking into consideration the items that we have equipped. To start with, we're extracting the player's Monster data from the dungeon player pointer. We're then setting the maxHP, `defence`, etc. to some default values. These are our base values that will represent the player's attributes without anything equipped.
Next, we setup a for-loop, to move through all the items we have equipped. We're testing if the entity at game's equipment at array index `i` is not NULL and then extracting the Equipment object from its `data` field. The equipment's `hp`, minAttack, maxAttack and `defence` are then added to the player's maxHP, minAttack, maxAttack, and `defence`. This will continue for each item we have equipped. As you can see, if we have a Crowbar equipped, our minAttack and maxAttack will increase. We call updatePlayerAttributes whenever we equip or unequip an item.
Finally, we're limiting the player's various attribute values. We're ensuring maxHP is never less than 1, that their `hp` is never more than their maxHP, that minAttack and maxAttack are never less than one, and that `defence` is never less than 0.
A simple function to follow. Resetting the player's attributes to their defaults each time helps us to avoid complicated math when it comes to adding and removing equipment, as we simply just add everything back up again.
The last thing we need to do is ensure our Crowbar, Biker Jacket, and Microchip are all available to us, by updating entityFactory.c:
We've just added in all the relevant init function calls.
And that's it! We can equip and remove armour and weapons. Not bad, not bad. But there's one thing that would be nice to know - how the thing we're equipping is going to affect our stats. What if we have several microchips and want to know which is better? In the next part, we'll look at how we can render a comparison of the current and new stats for both equpping and removing items.
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.