• 2D shoot 'em up
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a Run and Gun game —
So far, we can move our Gunner, as well as shoot. It would be good to have something to actually shoot at, however. In this part, we'll introduce some non-lethal practice targets for the Gunner to take out.
Extract the archive, run make, and then use ./gunner03 to run the code. You will see a window open like the one above, showing our main character on a black background, with a number of red and blue targets present. The controls from the previous tutorials apply. Shoot down the targets however you wish. The red targets will remain fixed in place, while the blue targets will move around, bouncing off the edges of the screen. Each one takes 3 shots to destroy and do not reappear once taken out. Once you're finished, close the window to exit.
Inspecting the code
Introducing the targets into the game has resulted in the need to make tweaks to a lot of existing code, as we'll soon see. For now, we'll start with looking at the additions and changes to defs.h and structs.h. Starting with defs.h:
We've added in two new defines, called EF_NONE and EF_WEIGHTLESS. EF is short for "entity flag". These are bitwise flags that can be combined together to add properties to entities. EF_NONE is a general value to say that the entity doesn't have any flags, whereas EF_WEIGHTLESS is a flag to say that the entity isn't affected by gravity. We'll see this one in action later.
Next, we come to structs.h, to see the changes made there:
To start off with, we've added three new fields to Entity: `dead`, `hitbox`, and `flags`. `dead` is a flag used to say that an entity has been killed and should be removed from the game world. It is 0 by default. `hitbox` will be used with collision detection, whereas the `flags` field will be used to store the flags added to the entity.
We've also added in a struct to hold the details of our Target:
Target is a very simple struct. Like Gunner, it is used to extend the base Entity. `life` is the amount of health the target has. It will be considered dead once this value hits 0. damageTimer is a variable used to help us visually identify that the target has taken damage.
We've also made some additions to Bullet:
`damage` and `hitbox` are both new fields. `damage` will control how much damage a bullet does to a target upon contact. `hitbox` is used for collision detection.
Now, let's look at how we're creating and handling the Targets. The code for the Targets is found in target.c. While we've got red and blue Targets that behave differently, they are both handled in the same file. Starting with initTarget:
We've mallocing and memsetting a Target, assigning it to a variable called `t`, then setting its `life` to 3. Higher values will mean our Targets will need more hits to destroy, while lower values will do the opposite. We're then assigning our Target to our entity's `data` field. The next thing we're doing is checking if we've loaded our required textures. We're doing this by testing if redTargetTexture is NULL. If so, we're grabbing our red and blue target textures, and assigning them to redTargetTexture and blueTargetTexture respectively.
Now we want to randomly select which type of Target this one will be. We're testing a random of 2 (0 or 1). If it's 0, this Target will be a blue type. We're setting the entity's `texture` to blueTargetTexture and also setting its `dx` to 4. Otherwise, we're setting the Target's `texture` to redTargetTexture and setting its `flags` as EF_WEIGHTLESS. This will mean that our blue Targets will immediately move to the right, while our red targets won't move at all (and won't even be affected by gravity).
We're then partly setting up the entity's `hitbox`. We're assigning the `hitbox`'s `w` and `h` (width and height) variables to be the same as the entity's texture's width and height. We'll see how the `hitbox`'s `x` and `y` fields are setup in a bit. Finally, we're assigning our entity's `tick`, `draw`, and takeDamage fields.
Now let's look at our `tick` function. It's actually quite simple (especially when compared to the player):
We're first extracting the Target from the entity's `data` field, then decreasing the damageTimer, capping it at 0. After that, we're testing the entity's horizonal position. If `x` is less than 25 pixels from the right-hand side of the screen (taking into account the target's texture width during the calculation), we're going to set it back to 25 pixels from the screen width, and then set its `dx` to -4. In effect, this means that when the target reaches the right-hand side of the screen (more or less), we'll reverse its direction, so that it bounces back the other way. The same is true of when the target reaches less than 25 pixels on the left-hand side. The position is reset to 25 and the `dx` is set to 4, to make it move to the right. Next, we're checking if the target is on the ground (onGround is 1). If so, we're setting the entity's `dy` to BOUNCE_HEIGHT (defined as the negative of double MAX_FALL_SPEED in target.h). This will make the Target bounce (in a somewhat unnatural fashion).
Finally, we're updating the entity's `hitbox`. We're only changing the `x` and `y` values of the `hitbox`, as the dimensions of the Target do not change. If we didn't update the hitbox like this, it would remain as the initial (0,0), meaning we'd had to shoot at the top-left of the screen to hit our targets. Again, we'll see more on hitboxes when we come to dealing with our bullets.
Our `draw` function is next. If you've worked through the SDL2 Shooter 2 tutorial, some of this will look familiar:
We're first extracting the Target from the entity `data` field and then testing the Target's damageTimer field. If it's 0, we'll draw the Target as normal, by calling blitAtlasImage and passing over its texture and position. If the damage timer is greater than zero, we'll draw the texture in red. We'll do this by calling SDL_SetTextureColorMod and passing in the main texture atlas (self->texture->texture) and RGB values (255, 32, and 32). With the red colour set, we then call blitAtlasImage as usual, before resetting the atlas color by again calling SDL_SetTextureColorMod and passing in the texture and 255 to every colour parameter.
This means that while damageTimer is greater than 0, the Target will be drawn in red, indicating that it has been hit. How is damageTimer set? That's actually done in the takeDamage function:
The function takes two parameter - the entity to which the damage applies and the amount of damage to apply. We're first extracting the Target from the entity's `data` field, then setting the Target's damageTimer to 4. Next, we're decreasing the Target's `life` by the amount of `damage` passed into the function. If the Target's life is 0 or less, we're setting the entity's `dead` flag to 1, to mark that it been killed and can be removed from the game. As we've seen earlier, setting the damageTimer to a number other than 0 will mean the Target will visually display it was hit.
That's it for the Targets. We can now look at the changes we've made to bullets.c. Since we now have things to shoot, we've added in collision detection to our bullet processing. Starting with the changes to doBullets:
As we saw when discussing the changes to the Bullet struct, we've added in a `hitbox` field (as an SDL_Rect). Now, after moving our Bullet (by adding `dx` and `dy` to the bullet's `x` and `y`), we're setting the bullet's `hitbox` `x` and `y` to the bullet's `x` and `y`. Right now, the bullet's width and height don't change, so we're good to leave those alone. We've also added in a call to a new function named checkCollisions, to which we pass in the bullet we're currently processing:
Our checkCollisions function is quite simple - it uses a for-loop to move through all the entities in the stage and check to see if they have a takeDamage function set. If so, it will next check if a collision has occurred between the bullet's `hitbox` and the entity's `hitbox` (by calling collisionRects and passing both SDL_Rects over). Should both of these things be true, we'll call the entity's takeDamage function, passing over the entity itelf and the damage the bullet causes. With that done, we'll set the bullet's `life` to 0, to kill it, and then return from the function. Since the bullet has hit something and has been killed, we're not interested in processing any more entities.
In case you're wondering what the collisionRects function (found in util.c) looks like, it can be found below:
This function merely takes two SDL_Rects and then delegates to the existing collision function. In short, collisionRects is a convenience function.
With the changes to bullets.c done, we can move onto entities.c. We've only updated the doEntities function, but with some important changes:
doEntities is the function where we see the EF_WEIGHTLESS flag in action. Now, instead of applying gravity to every entity in the world, we're first testing if it doesn't have the EF_WEIGHTLESS flag set in its `flags` field. We're doing this using a bitwise operation, meaning that if the flag is set, the evaluation will return a non-zero value. If the flag isn't set, no gravity will be applied. The red Targets have this flag set.
Entities can now be killed and removed from the game world, so we need to support this. We're doing this by checking if the `dead` flag is set. If so, we're removing the entity from the linked list (by assigning the previous entity's next as the current entity's next, and also ensuring that the tail of the list is correctly reassigned), before then adding the entity to a "dead list". This dead list will hold all the entities that have been removed from the world, and will be fully deleted later on. This helps us to avoid any crashes that result from pointers attempting to deference NULL data.
The last thing we need to do in order to support the Target is to add it to our entity factory. We can do this easily in initEntityFactory:
Like with the player, we need only use addInitFunc with the name of the entity and the entity's init function pointer in order for this to work.
We'll look next at how we're adding the Targets to the game world. It's rather simple. In stage.c, we've added one new function, which we've called from initStage:
addTargets is a temporary function to add all the target to our game. It doesn't do a lot:
We're calling initEntity 6 times, asking it to create us an entity called "target", which we assign to a variable called `e`. We're then positioning each created entity by assigning values to their `x` and `y` fields.
That's it for our Target shooting. Before we finish up, we should look a bit more at the hitboxes, as there is something we'll need to address in the next tutorial. If we return to structs.h, we've added a new variable to App:
In inner struct `dev`, thre is now a flag called showHitboxes. When this flag is set, the entity hitboxes will be displayed. This flag can be toggled by pressing F1, as we see in stage.c, in `logic`:
Pressing F1 inverts the value of the flag, so that showHitboxes will shift between 0 and 1. For the rendering of the hitboxes themselves, we've updated drawEntities in entities.c:
We're now checking if the showHitboxes flag is set. If so, we're looping through all our entities and drawing an outlined yellow rectangle using their `hitbox` data. The player, of course, has their own hitbox, which is updated during their `tick` phase:
We're simply assigning the `hitbox`'s `x`, `y`, `w`, and `h` values as the player's `x` and `y`, and their current texture's width and height. And this leads to a problem, since all our player sprites are the same size. As you can see from the image below, when we duck, our hitbox doesn't change:
So, in effect, ducking will do nothing but change the image. If we wanted to duck to avoid enemy fire, we'll be completely out of luck. In the next part, we'll look at how we're going to more effectively handle hitboxes to overcome issues such as this.
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.