• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— 2D Shoot 'Em Up Tutorial —
Note: this tutorial builds upon the ones that came before it. If you aren't familiar with the previous tutorials in this series you should read those first.
Our game wouldn't be much fun if it didn't present a challenge. Now the enemies can fire back and kill the player! Unpack the code and then type make to build. Once compiling is finished type ./shooter08 to run the code.
A 1280 x 720 window will open, with a near-black background. A spaceship sprite will also be shown, as in the screenshot above. The ship can now be moved using the arrow keys. Up, down, left, and right will move the ship in the respective directions. You can also fire by holding down the left control key. Enemies (basically red versions of the player's ship) will spawn from the right and move to the left. Shoot enemies to destroy them. Enemies can fire back so you should avoid their fire. Close the window by clicking on the window's close button.
Inspecting the code
To allow the enemies to fight back and destroy the player, a number of tweaks are needed to the code, not only to make the enemies fire, but also to handle the response to the player being killed. We'll start with some updates to defs.h:
We've added a constant here called ALIEN_BULLET_SPEED to control the speed at which the bullets fired by the enemies will move. We've also added another called FPS that will be used in various places for timing calculations. It is basically our 60fps cap that we've chosen to name rather than sprinkle 60 all over the code, since it could get confusing.
Next, we've updated util.c, adding a new function called calcSlope:
This function takes six arguments: the x and y of a src coordinate, the x and y of a destination coordinate, and two float references. What this function does is calculate the normalized step from one point to another. The dx or dy will always be 1 (or -1), while the other could be any value between -1 and 1. This will be used to tell the alien's bullets which way they need to go to reach their target when they're fired.
Moving on we see that once again it is stage.c that has seen the bulk of the changes. Beginning with initStage:
Since the enemies can fire back, we need a bullet texture for them to use. We're going to use a different one from the player's, in order to keep things distinctive. We're also now caching the player's ship texture into playerTexture. This is so that whenever we create (and recreate) the player, we don't waste time and resources by loading the texture again. Another change is that we've moved some of the initialization code for the stage into a new function called resetStage.
This function will do a number of things. It deletes any existing fighters and bullets, and then clears down the stage object (as well as restoring the linked list tails). It calls the initPlayer function and resets the enemySpawnTimer to 0. It also sets the value of a new variable called stageResetTimer. This is a variable that we will make use of when the player is killed. More on this below. Note that we use the FPS define here, to tell the counter to start at two seconds.
The logic function is the next one to see changes. This function has gained a few new lines:
The code now tests to see if player is NULL (which will happen in the case of them being killed by an alien bullet). If so, stageResetTimer will be decremented. Once it reaches 0 or less, the resetStage function will be called. We do this so that the stage is not reset instantly upon the player being killed, as this would be confusing and also look bad. Since player can now be NULL, we need to add a check to doPlayer to avoid a crash due to the NULL reference:
This is straightforward: we simply add a check for NULL on player to the whole of doPlayer. Nothing will be executed if player is NULL. Now for a new function: doEnemies:
doEnemies could be considered the AI call for the enemies. What this code does is step through each fighter, first testing to see if the fighter is not the player, whether the player is alive, and whether the fighter's decremented reload variable is 0 or less. If all these are true, the enemy can fire, and will call the fireAlienBullet function, passing itself over as an argument:
There are a number of similarities to the fireBullet function that is used by the player, but also some differences. We malloc an Entity to use as a bullet as expected, setting the texture to be the alienBulletTexture we cached earlier, and position the bullet in the centre of the attacker. We then calculate the direction the bullet will need to travel in order to hit the player with a call to calcSlope. We pass over the player coordinates and the attacker's, as well as references to the bullet's dx and dy so that the values can be set. We then multiply the dx and dy by ALIEN_BULLET_SPEED. Remember that either the dx or dy will be 1, meaning that the bullet will move at a constant speed of ALIEN_BULLET_SPEED along one axis. We next set the side of the bullet to be SIDE_ALIEN. This means that the bullet will only hit the player, passing through both the fighter that issued it, as well as any other ships that it happens to touch. Finally, we tell the attacker that it may fire again anytime within the next 2 seconds.
All this ultimately means that the enemies have multidirectional fire, in contrast to the player's straight shots. Now we need to make some changes to doFighters to accommodate the ability for the player to be killed:
We retain the test for the enemies moving off the left-hand side of the screen, setting their health to 0 if they do so. But now when we test if the fighter's health is 0 we also check to see if this is the player. If so, we NULL player so that we don't have a dangling pointer when we free it. Not doing so and not checking for NULL as we have done earlier would either lead to a crash or undefined behaviour (for example, it's possible the player might briefly turn into one of the enemies).
Another update we need to make is to tell the game to kill a bullet if it leaves the screen at any point and not just the right-hand side.
This is as simple as testing to see if the bullet's x or y coordinate (plus w and h) is outside of the screen and setting its health to 0.
We're almost done. Just one more tweak to make to spawnEnemies:
We want to make sure that the enemies don't open fire the moment they are created, but to wait a few seconds before doing so. We achieve this by setting their reload to 1 to 2 seconds, giving the player a chance to destroy them and not have to survive a hail of fire from the very start.
Finally, we don't want the player to be able to run off-screen, so let's clip player to the bounds of the playfield:
The code here should be obvious: it stops the player from leave the screen and also prevents them from moving forward any further than about the midway point.
And there you have it: the bones of a 2D shooter! There is still much that we can add to the game, such as effects, sound and music, scoring, and other bits, but all the pieces are falling nicely into place.
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.