• 2D shoot 'em up
SDL2 Rogue tutorial
SDL2 Gunner tutorial
SDL2 Shooter 2 tutorial
SDL2 Widget tutorial
SDL2 Adventure tutorial
— 2D Top-down shooter tutorial —
Note: this tutorial series builds upon the ones that came before it. If you aren't familiar with the previous tutorials, or the prior ones of this series, you should read those first.
This first tutorial will explain how to read the mouse in SDL2. Unpack the code and then type make to build. Once compiling is finished type ./bad04 to run the code.
A 1280 x 720 window will open, with a dark grey background over which a light purple grid is shown. A targetter will be shown that will track the mouse movements. The main character, Donk, will also display. Donk can be moved with the WSAD control scheme and will always face the targetter. Use the left mouse button to fire. The mouse wheel will cycle through the available weapons. The right mouse button will reload the pistol when it is out of ammo. Enemies will enter the screen from all angle and may drop powerups when defeated. Close the window by clicking on the window's close button.
Inspecting the code
We're going to add some enemies to the arena, for Donk to shoot. Right now, our enemies won't attack Donk, so the player is free to shoot them. We'll start by making some additions to defs.h and structs.h:
In defs.h we've added an enum to help us determine the side the entity is on. This is mainly to filter bullets and collision detection, so that enemies don't shoot one another and items don't get hit by bullets. Next, we've added some extra fields to Entity in structs.h:
We've added a field to hold the Entity's side, and also one to hold its radius value. This radius value will be used for collision detection: circle to circle. This is just the way we're going to handle collisions in this game, rather than use rectangular collisions which can look wrong due to rotation of entities. We've also got tick, touch, and die function pointers. These function pointers will be used to determine how the entity will respond in certain situations. We'll see more on this later. We've also added a new function to util.c:
The getDistance function will simply return a distance between two points. Nothing special.
Now for something bigger - a new file called enemies.c, which will be used to handle all our enemy logic. This file contains three functions: addEnemy, tick, and die. We'll go through them in order, starting with addEnemy.
This function creates an Entity and places it at the x and y parameters that are passed to it. The side, texture, and health are set, as well as the radius. The radius is much smaller than the size of the texture, meaning that collisions need to be more on target; shots can breeze by the outer portions of the enemy texture without making contact, which looks much better given its overall shape. We then assign the tick and die functions to two local functions of the same name:
The tick function calls getAngle to set the angle of the current entity to always face the player (passing in its own coordinates and those of the player). It also calls calcSlope to set its dx and dy to the player's position. In effect, this will mean that the enemy will always face and move forwards the player. Note that we're using a variable called self. This variable is a global Entity pointer that is assigned to an entity at various points during our logic processing. We'll see this happening when we come to the doEntities update later on. Finally, the die function:
When this function is called, there is a 50/50 chance that a random powerup will be created. The player's score will also be incremented by 10 points. We'll look at the addRandomPowerup function when we come to consider item creation and handling. Continuing with our enemy updates, we've made some changes to bullets.c:
doBullets now calls a new function called bulletHitEntity shown below:
This function simply steps over each Entity in the stage, checking to see if a collision is valid (the bullet's side is not SIDE_NONE and not the same side as the bullet itself, meaning enemies and the player won't kill themselves or allies). The distance between the bullet and the entity is then calculated and the tested against the radii of the two (sphere to sphere collision check). The two spheres will overlap if the sum of the two radii is less than the distance. If so, we kill the bullet and decrement the entity's health. This will allow our bullets to collide with entities, so that Donk can shoot targets.
Onto the changes in doEntities:
At the start of the loop, we're assigning our global self pointer to the current entity. We're then testing to see if the entity has the tick function assigned and calling it if so. We're doing the same with touch, calling a new function called touchOthers as needed. Finally, we're testing to see if the Entity's health is <= 0 and removing it from our linked list, calling the die function if needed. Our touchOthers function is quite simple:
As we can see, it simply loops through our entity list, checking that the entity is not itself, testing distances, and then calling touch as needed. Note that the current entity is known as self. In our case, the only entities that are using this function are items.
In order to add enemies to our game, a new function called spawnEnemy has been added to the logic function of stage.c:
Being in the logic function, spawnEnemy will be called every frame. The function itself is quite straightforward:
First, we decrement a variable called enemySpawnTimer (a static int within stage.c). When this hits <= 0, we'll place an enemy somewhere outside of the screen (left, right, top, bottom), done by a switch against a random of 4. We'll then call addEnemy, passing over the x and y we've assigned earlier and reset our spawn timer to between 1 and 2 seconds.
One of the final things we want to look at is items.c. Right now, powerups like ammo and health will randomly appear when an enemy is killed. The file contains quite a few functions. Starting with initItems:
initItems merely loads textures for use later. addRandomPowerup is the next function to consider:
We saw this being called when an enemy is killed. What this will do is randomly create a health, uzi, or shotgun powerup. Ammo is twice as likley to apper as health, being one chance in 5. We'll then either call addHealthPowerup, addUziPowerup, addShotgunPowerup. Each of these functions shares some common setup functionality, which we've put into a function called createPowerup:
The function basically sets up an Entity for use, setting the x and y, tick, and radius variables. It also sets the dx and dy, to make the item move in a random direction when it is created. The item won't move forever, however, as we can see in the tick function below:
The item's health is decremented with each call to tick, so that it will eventually vanish. The Entity's dx and dy are also multiplied by 0.98, causing the movement to slow. In effect, the item will slide for a brief moment before slowing to a stop.
With the general setup done, we can look at the creation of the individual powerups:
addHealthPowerup, addUziPowerup, and addShotgunPowerup all call createPowerup before then assigning their own textures and touch functions. This should all be very clear, so let's look at the touch functions next:
Notice that each of the touch functions takes an Entity as an argument. This matches with our function pointer signature in the Entity struct. Each of the touch functions performs very similar action. They each test to see if the Entity that has touched them is the player and, if so, set their own health to 0. After that, they do something specialized. uziTouch will increase the player's uzi ammo by 25, shotgunTouch will increase the shotgun ammo by 4, and healthTouch will give the player an extra point of life.
There is now plenty to do in our game, but still some things that could be done to make it more fun. For a start, the arena is too cramped. In the next tutorial we'll make it so that Donk can move around in a wider space. We'll also make the enemies fire at Donk, and introduce some new ones. Health and ammo might also be capped at starting values, so that it's no longer possible to hoard hundreds of life points and bullets.
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.