• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— Creating a Run and Gun game —
It's time to add in some new types of entities: solid ones. With the ability to add in solid entities, we can introduce doors, oil drums, and other things that can block the player's progress (or aid them). In this part, we'll look at the doors we've created, as well as oil drums.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./gunner09 to run the code. You will see a window open like the one above. The same controls from past tutorials apply. Oil drums can be shot and destroyed, or walked on; they are solid, so still stop the player from moving into them. There are also three types of doors: standard doors, that will open when the player walks into them (grey ones); weak doors, that look somewhat rusted; and checkered red doors, that require a red keycard to open. Explore the level and battle the enemies. Once you're finished, close the window to exit.
Inspecting the code
We've needed to make a number of changes in order to support solid entities, including adding in some entities to demonstrate how they will work. We'll start by focusing on doors and keycards. To begin with, we've made some updates to defs.h:
We've added in two new flags: EF_SOLID and EF_INTERACTIVE. When added to an entity's `flags`, EF_SOLID will declare that the entity is solid and that it should block the movement of other entities. EF_INTERACTIVE is a special type of flag to help with entity interactions. As we'll see later on, we'll only call an entity's `touch` function upon a collision. However, since we only test collisions if something moves, objects that don't move, such as Doors, will never perform a touch test. This flag is a hint to tell us to also test the door. We'll see more on this later.
We've also introduced a new enum:
This enum will be used to declare the types of Keycard that exist in the game.
We can see this enum being put to use in structs.h, where we've updated Gunner:
Gunner (our player object) how has a new field called hasKeycard, which is an int array. This array will act as a bunch of flags to say whether we've got a certain kind of keycard. Again, we'll see how this is used later on. We've also added in two new structs - Door and OilDrum, to represent a door and an oil drum. Door is simple:
It consists of a number of fields that reflect its state. `open` will tell us whether the door is open or closed (defaulting to 0, closed). `life` is the amount of life the door has, something that will only apply weak doors, that can be destroyed. `ty` is short of "target y". This is the y position of the door when it's open. requiredKeycard is the type of card that is required by the door before it will open. This only applies to locked doors. damageTimer is a field we've seen many times before, and is used to visually display that the door has taken damage.
OilDrum is just as simple:
It consists of just `life` and damageTimer fields. We know what these do by now.
The final struct is Keycard:
It simply defines its `type`, which could be one of KEYCARD_RED, KEYCARD_GREEN, or KEYCARD_BLUE.
Let's now head over to entities.c, where we've added in the support for solid entities. The first thing to be aware of is that our touchOthers function has been removed. This was always a temporary function, so this should come as no surprise. The changes we've made to support solid entities and interactions now happens elsewhere. To being with, we've updated the `move` function:
Now, after calling moveToWorldY (and moveToWorldX), we're calling a new function named moveToEntities. It takes three parameters: the entity that we're currently moving, the x amount that we're moving, and the y amount that we're moving. Notice how our first call to moveEntities (within the e->dy test) passes 0 for the x amount and the entity's `dy` for the y amount. This is because we're only interesting in moving along the y axis to begin with. The second call passes the entity's `dx` and 0 for the x and y values respectively. Again, we're now only testing the x direction, as this is all we're interested in.
In short, like as when we test moving against the world, we test first the y direction and then the x direction. Never both at once. This helps us to keep our movement response correct, knowing which direction we were moving when the collision occurred.
Now let's look at the moveToEntities function itself:
It might look like there's a lot going on, but it's rather simple, as we'll see.
The first thing we're doing is testing the value of `dy` (that was passed into the function). If the value is greater than 0 and less than 1, we're assigning a variable called `y` a value of 1. Otherwise, `y` is set to 0. The reason for this is quite important. The value of `y` is being set to allow us to "trace down", to test for collisions while the entity is falling. Why this important is simply due to our frame rate. At very high frame rates, an entity's `dy` will only increase a tiny amount under the influence of gravity. This increase in `dy` could end up being so small that contact with an entity directly beneath us would be missed. Remember how at the start of our entity processing we are setting an entity's onGround flag to 0 and also increasing its `dy` due to gravity? That tiny increase to `dy` could mean that it is several frames before our collision detection with an entity directly beneath us is registered. Thus our current entity will keep moving between freefall and on the ground many times a second, even if they're standing on solid entity. For the player, this makes them animate quickly between standing and falling. This is basically error correction.
With our error correction determined, we begin looping through all the entities in the stage, assigning them to a variable called `other`. We next check if `other` is not the same entity as `e` (so that an entity doesn't try and collide with itself), and then check for the actual collision by calling the `collision` function. We're passing through `e`'s `x` and `y` coordinates (plus the value of `y` we determined earlier) and it's texture's width and height. We're also passing through the same for `other`. Remember that we're not using hitboxes here, as we're only using those to support bullet-entity collisions and line-of-sight checks.
If a collision has been found, we then want to see if the entity we've hit is solid, by checking its `flags` for EF_SOLID. If it is, we want to correct our position. We first check to see if we're moving along the y axis, by testing if `dy` is a non-zero value. If it is, we'll want to align our entity depending on the direction we were moving. If we were moving down, we'll set our entity to the top of the thing it hit. If we're moving up, we'll set it to the bottom. You will likely recognize this collision response as more-or-less the same as when we adjust due to a map tile collision. Note how we pick `e`'s `texture` or `other`'s `texture` for the placement adjustment depending on whether we were moving up or down. With our entity's `y` position corrected, we test to see if `dy` was greater than 0. If so, it means that we were falling and should therefore now set the entity's onGround flag to 1. Finally, we zero the entity's `dy` to stop it from moving.
We next test to see if we were moving along the x axis (`dx` is a non-zero value). If so, we're making the same adjustment as we did with the y axis, except that we're using the entities' `x` value and texture widths. We're also zeroing the entity's `dx` when we're finished.
With our position correction performed, we're free to now test call our `touch` functions. We first test to see if `e` has a touch function set and call it, passing over `other` as the second parameter (the thing it touched). Next, we're checking to see if `other` has the EF_INTERACTIVE flag set. If so, we'll also call `other`'s `touch` function (if it is set) and pass over `e` as the thing that was touched. In effect, we're checking if our moving entity has touched something and also allowing the thing that was hit to touch the entity that moved into it. Again, this is so that we can support touch functions for entities that don't natually move (such as doors).
That's all that we need to do in order to support solid entities, and now means that we can actually introduce some. Since we mentioned doors just now, we can start with those. Our game supports 3 main types of doors: normal, weak, and locked (which will require one of three keycards to open). All our doors live in doors.c. We'll first start with looking at the regular door, one that opens when we walk into it.
We'll look first at initDoor:
initDoor is a general purpose function for making a door. It's a static function, so it's only accessible from within doors.c. The first thing this function does is malloc and memset a Door. We then set the Door to the `data` field of the entity that was passed into the function, and set its `flags`. We want our door to ignore gravity (EF_WEIGHTLESS), be solid (EF_SOLID) so that things cannot pass it, and also make it interactive (EF_INTERACTIVE) so that it can respond to objects that move into it (if you want to see what happens without this flag, simply remove it and walk into a regular door - it won't respond). We then set its `tick` and `draw` functions. These two functions perform the same role for all door types, as we'll see in a bit.
Now, let's look at what goes into creating a regular door. The initNormalDoor function is there for that:
The first thing we're doing is calling initDoor, to set all the common door data. Next, we're loading in a texture called "gfx/sprites/normalDoor.png", if needed, and assigning it to a variable called normalDoorTexture. This texture is set to the entity and will be the image drawn for our door. We're then setting the entity's `hitbox` width and height to the same as its texture's width and height, before finally setting its `touch` function to touchNormalDoor. A pretty standard init function.
We'll come to the touch function in a bit, but first let's look at the shared `tick` and `draw` functions. Starting with `tick`:
The main purpose of `tick` is the raise the door if it's open and to update its damageTimer and `hitbox`. After extracting the Door data, we're testing its `open` flag. If it's 1, we're next checking if the door's `y` is greater than it's `ty`. If so, we're decreasing the value of the door's `y`, to make it move up the screen, limiting it to the Door's `ty`. In effect, when the Door's `open` flag is set to 1, we'll reposition it's `y` value to `ty`. In this function, we're also decreasing damageTimer, limiting it to 0, and setting the door's `hitbox`'s `x` and `y` to the door's `x` and `y`.
Our `draw` function is next:
This is something we've seen many times before - we're drawing the door normally, unless its damageTimer is a non-zero value, whereby we're rendering it in red, to indicate damage.
Moving onto the touchNormalDoor function. This is where things get a bit more interesting:
Remember that doors don't move by themselves, and therefore rely on another (moving) entity to make contact with them before this function will be invoked. We're first checking if the thing (`other`) that has made contact with the door is the player. If so, we're extracting the Door struct from `self`'s `data` field, and checking the state of its `open` flag. If it's 0, we're setting the value of Door's `ty` to the value of its `y`, minus its texture height (itself less 8 pixels). This means that when open, the door will raise to almost its full height. The reason for not quite raising it fully (the 8 pixels fewer) is merely for aesthetic reasons. With that done, we set the door's `open` flag to 1, to now mark it as open. Now, when `tick` is called, the door will move to its `ty` position.
We'll look at the weak door now. This door doesn't open when we walk into it and doesn't require a keycard. Instead, it must be shot to destroy it. It's created via a function called initWeakDoor:
As with the normal door, we're calling initDoor and also fetching the weak door's texture (assigned to weakDoorTexture). Next, we're extracting the Door from the entity `data` field, that we created in initDoor, and setting its `life` to 16. This means the door will require 16 hits before it buckles and dies. Quite a strong door, to be fair. We're also setting the door's `texture` and `hitbox`, again assigned from the texture's width and height. The weak door doesn't have a `touch` function, but does have a takeDamage function:
Before we're applying the damage to the door, we're first testing to see who the `attacker` is (for example, the owner of the bullet that caused the damage). If the `attacker` is the player, we're extracting the Door from the entity `data` field and reducing the Door's `life` by `amount`. We'll mark the door as dead if its `life` falls to 0 or less. We're also setting the Door's damageTimer to 2, so that when we render it we do so in red, to show it has taken a hit. All this means is that only the player can damage and destroy the door. Shots from enemy soldiers will strike the door, but otherwise do nothing.
The last door to look at is the red door. The red door operates like a normal door, except that it requires a red keycard in order to open. Its init function is named initRedDoor:
Much like the other doors, we're calling initDoor and grabbing the required texture for the door, as well as setting the `hitbox` data. We're also setting the Door's requiredKeycard field to KEYCARD_RED. We're finally setting the Door's `touch` function as touchLockedDoor:
Like the normal door, we're first checking if the thing that has touched the door is the player. If so, we're extracting the Door from `self`'s `data`, then checking if the door is not already open. If it's not, we're pulling the Gunner from `other`'s `data`, then testing the Gunner's hasKeycard array to see if the keycard we need to open the door is present; the Door's requiredKeycard matches the index within the Gunner's hasKeycard array, making it easy for us to test this. If all these conditions hold true, we open the door in the same fashion as the regular door.
We're done with Doors. As you can see, they were quite simple to create. Now we can look at how our Keycards work. They live in a file called keycards.c. The file contains a number of functions, so we'll start with initKeycard:
Like initDoor, initKeycard is a static function, that is used to setup some common attributes for our Keycard. It takes two parameters: the entity to use (`e`) and a variable called type, that represents the type of keycard. We start by mallocing and memsetting a Keycard struct, then set its `type` as the value of `type` we passed into the function. The entity's `data` field is set as the Keycard, and we set the `tick`, `draw`, and `touch` functions.
initRedKeycard is next. As its name suggests, this function deals with creating a red Keycard:
We're first calling initRedcard to set up all the common attributes and functions, and then checking if we need to load the required texture (if redTexture is NULL). With that done, we assign the entity's `texture` and set its `hitbox`'s width and height. It's quite likely this function could be broken down further when we add more keycards, so we'll likely do this in a future part.
The next two functions, `tick` and `draw`, are quite basic. Starting with `tick`:
We're just updating the hitbox coordinates. `draw` does little more than render the keycard:
Our `touch` function is a bit more interesting:
Like the Doors, we're testing to see if the thing that has touched the keycard is the player. If so, we're extracting the Gunner data from `other` and the Keycard data from `self`. We then set the appropriate index in the Gunner's hasKeycard array to 1, using the Keycard's `type`. Since the Keycard's `type` and the hasKeycard array align with the KEYCARD_* enum, we don't need to do any more checks. With that done, we set the keycard's `dead` flag to 1, to remove it from the world.
The last new entity that we've added is an oil drum, that can be shot and destroyed. All the functions for our oil drum live in oilDrum.c. We'll start with initOilDrum:
We start by mallocing and memsetting an OilDrum, then setting it's life to 12. Oil drums are tough, it seems, even tougher than doors! We then want to grab our textures. You will have noticed that the oil drums are assigned a random image each time the level is started. The image has no baring on the attributes of the drum, however. `textures` is an array of AtlasImages, of length NUM_TEXTURES (3). We're testing the AtlasImage at index 0, to see if it's NULL, meaning we need to load our textures. If so, we're loading all three of our textures (gfx/sprites/greenOilDrum.png, gfx/sprites/blueOilDrum.png, and gfx/sprites/greyOilDrum.png) and assigning them to indexes 0, 1, and 2. We next tell our drum entity that it is solid, by setting the `flags` to EF_SOLID. Next, we're assigning the OilDrum (`o`) to our entity's `data` field, and then giving it a random texture, using the result of rand() NUM_TEXTURES against our `textures` array. The `hitbox` width and height are set to the same as the texture, and the `tick`, `draw`, and takeDamage functions are set.
In summary, our initOilDrum function sets up a solid entity with a random texture. The remaining functions are ones that we've largely seen before, so we won't linger when we discuss them. Starting with `tick`:
We're just reducing the OilDrum's damageTimer, limiting it to 0, and also updating its `hitbox`'s x and y. Equally, `draw` doesn't need much explaination:
Like other entities that can be damaged, we're checking the OilDrum's damageTimer to see if we want to render it in red or simply draw it regularly. The takeDamage function holds no surprises either:
We're reducing the OilDrum's `life` by `amount`. Note how we're not checking to see if the `attacker` is the player. This means that the enemies can also damage and destroy OilDrums. This might be something that is tweaked in future, for the sake of consistency with weak doors.
That's all our new entities done, and we're almost finished. What we should look at now is the update to the HUD. You will have noticed that there are three slots beneath the player's health, one of which displays the carried Red Keycard when it is picked up. We'll look at how that is done now. Turning to hud.c, we'll start with initHud:
We're loading four new textures, three to show the types of keycard we might be carrying, and an image to show an empty slot. Our `keycard` variable is an array of AltasImages (defined as static, with a size of KEYCARD_MAX). We're loading the appropriate image into the relevant `keycard` index, and loading the "empty" image into keycardEmpty.
Next, we've updated drawHud:
We've added a line to call a new function named drawKeycards, into which we're passing over the Gunner data. drawKeycards is somewhat similar to drawPlayerLife:
We're first assigning a variable called `x` a value of 20. This is the starting horizontal position of our keycard images. Next, we're setting up a for-loop, up to KEYCARD_MAX. We're then using this to check if our Gunner is carrying a certain keycard, by testing its hasKeycard value at index `i`. As the Gunner's hasKeycard array order and our keycard texture array order mirror our for-loop order, we can render our carried keycards with ease. If hasKeycard at index `i` is 1, we're calling blitAtlasImage, passing in the keycard AtlasImage with the same index, as well as our `x` value. If hasKeycard is 0, we're calling blitAtlasImage and passing through keycardEmpty, to show that we don't have the keycard. After each loop, we're increasing the value of `x` by the width of keycardEmpty, plus 15 pixels for padding. All our keycard images are the same width, so we can use keycardEmpty's width with confidence here.
The very last thing to do is enable the creation of the new entities we've added into the game. As with all other entity types, we need only add these into initEntityFactory:
With the relevant externs added to entityFactory.h (initNormalDoor, initWeakDoor, etc), we can now have our doors, oil drums, and keycard in our map.
This was a very long part, though in fairness a lot of it came down to discussing things like `touch` functions, etc. In the next part, we're going to focus on just one thing - a quadtree. We have very many collision checks happening each frame now. In fact, throwing in some simple debug shows that we're making 550 entity collision checks per frame, a number that increasing significantly when bullets start flying. This is very, very high for such a small map. Imagine how many checks would be performed in a map several times larger? Our quadtree will break up these collision checks, to only test things that are relevant within the current context.
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: