• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— Creating a vertical shoot 'em up —
Up until now, our game has been very one-sided. The enemies can't fire back and we can upgrade our weapons, making it even more unfair. In this part of the tutorial, we'll be making a few gameplay tweaks: the enemies can now fire back, points are now awarded for clearing a wave, and enemy attack waves are now somewhat randomized. In order to demonstate the changes, no power-up pods are available.
Extract the archive, run make, and then use ./shooter2-04 to run the code. You will see a window open like the one above. Use the arrow keys to move the fighter around, and the left control key to fire. Dodge the enemy fire, shoot the aliens, and collect the points pods. If you're killed, the game will restart after a few seconds. When you're finished, close the window to exit.
Inspecting the code
We've made quite a lot of changes and additions, so this will be a somewhat lengthy part. One of the first things you'll notice is that there are now explosions when killing the aliens (and when getting killed yourself). We've updated structs.h to define what our explosion should be:
Our explosion is animated, using a number of frames from the AtlasImage. Note how the `texture` field is a pointer to a pointer. We'll see why this is when we come to talking about the explosions. The explosions are also a part of a linked list, hence the `next` field.
As mentioned earlier, we updated our SwingingAlien, so that its attack patterns have some variation to them:
The SwingingAlien now has fields for swingAmount, sweepRange, and `dy`. On top of that, it also has a `reload` variable, for use with shooting.
We've also updated Stage:
Beforehand, we had a variable called hasAliens, to tell us if any aliens were present. Now, we have a variable called numAliens, to use as a counter.
Those are the changes to structs.h. Since the focus of this part is on the enemies firing back, we'll move straight onto that. We've changed swingingAlien.c a bit for this purpose. We'll start with initSwingingAlien:
We're now passing in some extra parameters to the function - `x`, swingAmount, sweepRange, and `dy`. `x` will be the starting position on screen, as we're no longer hardcoding that to the horizontal centre. swingAmount and sweepRange will both be used to determine how much the SwingingAliens move back and forth across the screen. Depending on the values, the aliens could have very tight swinging patterns that barely move across the x axis, to large movements that cross the entire screen. The values fed in will depend on the randomness of the wave. `dy` will tell the alien how fast it will move down the screen. All these values are set into SwingingAlien struct.
The next thing we're doing is grabbing the bulletTexture. This is the image that will be used for the alien bullet when it fires. Note that we don't need to worry about testing if it's NULL, as testing littleYellowAlienTexture covers this.
Now, onto the `tick` phase, where we've updated the alien's logic:
A few things have slight changes - instead of hardcoded amounts for updating `swing` and `x` values, we're using swingAmount and sweepRange. The alien's `y` is also being updated using the new `dy` variable.
The two major changes follow this. We're now decrementing the SwingingAlien's `reload` variable, limiting it to 0. Once it reaches 0, we're going to give the alien the chance to fire. We're grabbing a random value of 0-4 and checking if it's 0 (so, a 1 in 5 chance). If it's 0, we'll call fireBullet, a new function. Notice how no matter what the outcome is, we're always resetting the SwingingAlien's `reload` to FPS. This means that the alien will only ever fire once a second, at most. Our chance is 1 in 5, so each alien will, on average, fire once every 5 seconds. This will stop the player from being overwhelmed by a hail of bullets.
Next, we're checking if the player has come into contact with the alien. If there's a collison between the two, we're setting both the alien's `health` and the player's `health` to 0, and then calling each entity's `die` function. We're checking if the player's `health` is greater than 0 before doing so, so that a dead player doesn't continue to kill other aliens..!
Finally, we're bumping Stage's numAliens counter by 1. Each alien will push this number up, to tell us how many aliens currently exist.
The `die` function has seen some tweaks, too:
When an alien is killed, we're calling a function named addExplosion, passing in the alien's `x` and `y` values, plus half its width and height. This will create an explosion centered about the alien. Next, we're decrementing the value of Stage's numAliens. If it reaches 0, we're adding a PointsPod. This means that points will only be awarded for clearing the entire wave. We might tweak this in future, to always award a single point per alien killed, with the points pods offering a bonus (such as 10 or 25 additional points).
Let's push onto bullets.c. Now that aliens can fire bullets, we need to make some changes to doBullets, in order for them to affect the player:
We've added in a few if tests, to see if the bullet is owned by an alien (by testing the bullet owner's type). If it's owned by an ET_ALIEN type and the player is still alive, we're calling a new function called doPlayerCollisions:
This function is testing whether a collision has occurred between the player and the bullet. If so, the player's `health` is set to 0 and the player's `die` function is called. The bullet itself is also flagged as dead, and we're adding in a small explosion at the point where the bullet hit the player (this is something we're also doing in doAlienCollisions when a bullet strikes an alien). All in all, not a unexpected piece of logic.
Since we've mentioned it a couple of times, we should look at what the player's `die` function does:
The only thing we're doing is adding an explosion, just like when the aliens die. Nothig more. However, while we don't have the ability to gain sidearms in this part of the tutorial, let's have a quick look at the change to `tick` in sidearms.c:
The Sidearm `tick` is testing the player's `health`. If its fallen to 0 or less, the sidearm will add an explosion of its own and set its `health` to 0. In effect, the sidearm will die if the player does. Again, we can't gain sidearms in this part, so this just some preemptive code for when we can.
Now for something important. There's been a change to how the entity processing works, as since the player can die there is potential for crashes to occur, due to the player entity being deleted. As such, the doEntities function no longer deletes entities right away:
Now, when an entity's `health` falls to 0 or more, we're removing it from the main entity list as before, but instead of freeing all its data, we're throwing it into a new list. This list, declared static in entities.c, consists of a deadHead and deadTail. Again, the reason for doing this is because there are many parts of the code that test the player entity, for collisions and such. If they do this against the player entity pointer once the data has been freed, the game will crash. We're therefore moving everything that is killed into a dead list, allowing them to still be referenced, and will delete them properly later.
As an example, consider the `tick` function for the sidearms (above). The sidearms are constantly aligning themselves to the player. If the player is deleted, then the sidearms will attempt to dererfence a NULL pointer and we'll be hit with a crash. Shifting everything flagged for removal into a separate list overcomes this issue nicely.
When we want to properly delete all the entity data, we call upon a new function called clearEntities:
This function uses a while-loop to see if an entity exists after the head of the list. If so, we'll grab a reference do it, then point the head of the list at that entity's next, effectively cutting it out of the list. With it no longer in the list, we're freeing the data as normal. The while-loop will continue to remove all entities until the list is empty. Finally, we're assigning the entityTail to the entityHead, to ensure it's not pointing at a NULL reference.
We have a similar function for clearing the list of dead entities:
Now, while it's only the player entity that is affected by the NULL crash issue, and we could've handled it as a special case in the doEntities loop, it doesn't exactly feeling in keeping with the rest of the code; the doEntities function shouldn't be concerned itself with such specifics. The dead entities list might also come in useful for other things later on, such as other power-ups and weapons.
As we've added in explosions to our game, we should look at how these are handled. We've created a file called effects.c, where we're doing all our explosion processing. There are a number of functions in this file. We'll start with initEffects:
Our explosions are a linked list, so the first thing we're doing is preparing the list by memsetting explosionHead and assigning the explosionTail. Next, we're testing if we've loaded our textures. Our textures form an array of frames, so we're going to test a flag called hasTextures to see if we've loaded them. hasTexture is initially set to 0, so the first time initEffects is called, we'll load in the textures. We'll update hasTextures to 1, to tell the code not to load them anymore, and then set the textures for our smallExplosionTexture and explosionTextures. We'll set up a for-loop for each array, and create a filename for each wanted texture, using sprintf, passing this to getAtlasImage to grab the texture for each array index. So, we'll load gfx/smallExplosion01.png, gfx/smallExplosion02.png, etc.
doEffects is next, and it's where all the effects are processed:
We're looping through all our explosions, decreasing their frameTime. When the frameTime falls to 0 or less, we're incrementing the explosion's `frame` and resetting the frameTime to the value of frameSpeed. We're then testing to see if the new value of `frame` is less than numFrames. In other words, we're ensuring that there are still frames left to draw in our explosion. If so, we'll increment the `texture` pointer. Remember that the `texture` field in Explosion is a pointer to a pointer. `texture` is pointing to an array, so increasing the pointer value will move to the next index in the array. However, if the value of the explosion's frame is equal to or greater than the total number of frames, we'll consider that the animation is done and delete the explosion. This is done in the standard way that we free an item within a linked list.
Drawing our effects is simple, as we can see from the drawEffects function:
We're just looping through each of our explosions and calling blitAtlasImage. We need to use `*e->texture` here, instead of `e->texture`, as the `texture` field is a pointer to a pointer, so it needs to be dereferenced. We're also passing in 1 to the `center` parameter of blitAtlasImage, so that our explosions are centered around their position. Our explosion frames aren't all the same size, so if we didn't do this, things would look odd.
As we've seen, there are two functions for adding explosions - addSmallExplosion and addExplosion. These functions will create a small and normal sized explosion respectively. We'll look at addSmallExplosion first:
The functions takes `x` and `y` coordinates as parameters, for the explosion's position. We first malloc and memset an Explosion, then add it to our explosions list. After that, we're setting the Explosion's `x` and `y` variables, the number of animation frames (numFrames) as NUM_SMALL_EXPLOSION_FRAMES, the frameSpeed, the frameTime, and the explosion's `texture`, pointing it at the smallExplosionTextures array. With this done, we'll have a small explosion effect that will show very briefly before it is removed.
The addExplosion function is much the same:
The only major differences here are the number of frames (NUM_EXPLOSION_FRAMES), the frameSpeed, and texture array that we point to. This explosion will last a little longer, due to the frameSpeed being higher.
The final function is one that clears all the effects in the linked list:
This function acts like the clearEntities function, in that it uses a while-loop to remove the first explosion in the list until there are none left.
With all that done, we should finish off by looking at what we've changed in stage.c, to get it all working. Starting with initStage:
We have a new variable called gameOverTimer that we're setting to 2 seconds. What this will do is make it so that when the player is killed, there will be a two second pause before the game restarts. We can see this in action in the logic phase:
The first thing we're doing in the logic phase is resetting the number of known aliens, updating Stage's numAliens to 0. Futher on, we're testing to see if the player has been killed, by testing if their health has fallen to 0 or less. If so, we'll decrement the gameOverTimer. Once that reaches 0 or less, we'll call resetStage and initStage, to clear down the stage data and set everything back up again. If the player is still alive, however, and there are no more aliens in the stage, we'll call clearDeadEntities and nextWave.
resetStage is a new function we've added:
As you can see, it just makes calls to the various clear function that we defined in this part of the tutorial. Consider this to be a teardown function for the game stage, while initStage is the setup.
That's this part concluded. You've likely noticed that the highscore isn't retained between stage resets. This is something we'll fix in the next part. We'll also restore the power-up pod, but with a caveat - it'll be held by an alien that must be destroyed before it escapes! We'll also introduce some new enemies, ones that will require more than one shot to kill and have new attack patterns.
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.