— 2D Shoot 'Em Up Tutorial —
Introduction 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. We're now ready to do something important in our tutorial: firing a bullet. Extract the archive, run cmake CMakeLists.txt, followed by make to build. Once compiling is finished type ./shooter05 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. Close the window by clicking on the window's close button. Inspecting the code Now we're getting somewhere. We've got a ship that can move about the screen and unleash a volley of fire. This is a result of the refactoring that we've carried out. There have been quite a few changes behind the scenes and maybe a bit more to get your head around. Function pointers and linked lists are now involved. It shouldn't be a huge mountain to climb, but we need to understand the principles behind all this. Let's have a look at what's changed. First off, let's investigate structs.h.
The first thing to note is that we've added a new struct: the Delegate. This struct, as it's name suggests, will act as a delegate for handling the logic and draw functions in the game's main loop. We'll see this in action later. The next update is to App. Instead of the up, down, left, right, and fire variables for holding the keyboard state, we now have one array called keyboard for holding the state of all keys on the keyboard (or as many as MAX_KEYBOARD_KEYS allows). Again, this will be seen in action later. Coming to the Entity struct, we have changed the type of x, y, dx, and dy to float, added a new variable called reload (used for tracking the player's ability to fire), and a pointer to another Entity called next. This is for use with linked lists. Finally, we have another new struct called Stage. This will be used to hold information about fighters and bullets. We should briefly look at defs.h, to see what's changed.
We've added three new #defines: PLAYER_SPEED to control the speed of the player; PLAYER_BULLET_SPEED, to determine how fast the player's bullets should move; and MAX_KEYBOARD_KEYS, as used by the App struct. With that done, let's take a look at input.c:
The doKeyUp and doKeyDown functions have changed quite a bit, being reduced to just four lines each. In both, we are using the SDL event's scancode value to set the respective value in our app.keyboard array. We test to see if the event is not a keyboard repeat and also that the scancode is less than MAX_KEYBOARD_KEYS to prevent errors. Now we come to stage.c. This is a new compilation unit and a fairly large file. This file is responsible for handling the actual game logic. Since it's larger than others, we'll break it down into sections and deal with those one at a time. We'll first start with the initialisation functions:
The initStage function sets up a number of things for playing the game. It assigns the delegate's logic and draw pointers to two static logic and draw functions of its own, and also prepares the fighter and bullet linked lists for use. It then calls the initPlayer function. This is more or less the same code that we had in main.c, except that the player object is now malloc'd and added to the fighter linked list. We also set the player entity's w and h (its width and height) based on the size of the texture, by calling SDL_QueryTexture. We also precache the bullet's image into bulletTexture, so that we don't keep loading this (and wasting memory) whenever we create a bullet. With the setup done, we can move onto the logic phase of stage. There are four functions involved in this, but they are simple to understand:
The logic function merely calls doPlayer and doBullets, nothing else. Because this function is linked to the app.delegate's logic pointer, it will be called every loop in our main function (more on this later). We'll consider doPlayer next.
doPlayer is again what we used to have in main.c, although there are a few differences. We are now testing the app.keyboard array to see if our control keys are active (up, down, left, right, and fire). We're also decrementing the player reload variable if it's greater than zero. This variable controls how fast we can fire. When we come to fire, we test to see if the fire key is pressed and also if the player's reload is 0. If it is, we call the fireBullet function. One thing to notice is that we're not directly updating the player's x and y coordinates when moving, but instead the dx and dy. We're then adding the dx and dy to the x and y at the end of the function. Although this isn't strictly needed now, the next tutorial will make proper use of this. The fireBullet function is again quite simple:
We create an Entity object and add it to the bullet linked list. We next assign the bullet's x and y coordinates to those of the player, set its dx to PLAYER_BULLET_SPEED (which in this case will make it move rapidly to the right), assign its health to 1, and assign its texture from the one we cached earlier. Another thing that we do is position the bullet y coordinate a little more centrally to the player. This is done by using the player's h and bullet's h. Finally, the set the player reload to 8, telling the game that 8 frames (approx 0.133333 seconds) must pass before we can fire again. We're almost done with the logic phase of stage. We finally come to doBullets:
This function effectively loops through our linked list of bullets, moving each one (by adding the dx and dy to the x and y respectively). As before, if the bullet reaches the right-hand side of the screen, we delete it (linked list handling is somewhat beyond the scope of this tutorial, but we are essentially chopping it out of the chain and updating the reference the previous bullet had to it). We're now able to move onto the draw functions. There are three functions involved in this, all very simple in nature:
Like our logic function, our draw function will be called each loop in main. Our main draw function calls drawPlayer and drawBullets. drawPlayer does just that: draw's the player texture using the blit command, as we've seen before. The drawBullets function isn't too different, stepping through the linked list and drawing each bullet at its position on the screen. Finally, we can take a look at main.c. There have been a few tweaks here:
Gone are all the references to player and bullet, since they have moved into the stage object. Now we call initStage to setup the main game, and, as discussed earlier, call app.delegate.logic and app.delegate.draw each loop to perform the game logic and draw the scene. If that looks a little odd, remember that these are function pointers and will be calling the logic and draw functions in stage.c. One other function we have added is capFrameRate:
This replaces our SDL_Delay(16) and does some funky maths to attempt an accurate 60fps lock. This works by testing how long it took to render the previous frame and adjusting our SDL_Delay value appropriately. The remainder comes into play due to 1000 / 60 = 16.66667. This is an accumulator that will help to keep us closer to 60fps rather than 62fps. It works well for the most part. That's it for the refactoring of the code. We can now build up to something much more exciting and actually face off against enemies, add collision detection and all sorts. Our code will be much easier to read, maintain, and expand as a result of these changes. Exercises
Purchase The source code for all parts of this tutorial (including assets) is available for purchase: From itch.io It is also available as part of the SDL2 tutorial bundle: | |
Desktop site |