— Mission-based 2D shoot 'em up — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction It's time to move onto shooting things. In this part, we're going to spawn some enemies for the player to destroy. These will be static fighters that won't move or attack the player. That will come later. For now, enjoy battling an unlimited number of enemies (take note not to wander too far away from the enemies, or they will be near-impossible to find again, as there is no radar or direction indicators available). Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter3-03 to run the code. You will see a window open like the one above. Use the WASD control scheme to move the fighter around. Hold J to fire your main guns. Shoot the red enemy fighters that appear to destroy them. Each enemy requires 3 shots each to be destroyed (when the KIT-E has a damage rating of 1 - you can modify this in game.c). When the enemy is destroyed, a new one will appear randomly around the player, for a maximum of 5 at any one time. When you're finished, closed the window to exit. Inspecting the code Adding in enemies is a very simple task, since it's just a case of using the entity factory and placing the resulting enemy near to the player. Of course, we've made a bunch of other changes to the code to support destroying them, so let's take a look. Starting, as always, with structs.h:
We've added two function pointers: takeDamage and `die`, that will handle an entity taking damage (from a bullet impact) or also for what happens when it is destroyed. Next, we've updated Fighter:
We've added a field called hitTimer, that will be used to control the flash that occurs when a fighter (either the player or an enemy) is hit. A new struct has been introduced, called Debris:
Our Debris struct will represent the small trails of explosions that randomly move around after a fighter has been destroyed. We'll touch on how these work a little later on. Finally, we've updated Stage.
We've added in two new linked lists: one for dead entities, and another for our debris. Another variable that we've added is numActiveEnemies. This will hold the value of the number of enemies that are currently active on the stage (i.e., the ones that the player is battling). This will come in very useful later on when we're working with objectives, etc. Now, let's move onto our enemy definition. We've added in a compilation unit called greebleLightFighter. It contains a number of functions, so we'll work through them one at a time, starting with initGreebleLightFighter:
A very standard entity init function. Much like the player, we're creating a fighter and assigning all the various attributes to it. Notice how we're assigning the takeDamage and `die` function pointers. The `tick` function comes next:
Again, very much like the tick function for the player. One thing that's different is that we're incrementing Stage's numActiveEnemies. The `draw` function is also near-identical to that of the player:
The `die` function is the final one to look at. It's quite simple:
This function is called when the enemy is killed (in its takeDamage function, that'll we'll look at in a bit). We're doing three major things here - adding in a load of explosions around its centre, via a call to addExplosions (25 is the number of explosions we wish to generate), we're throwing out some debris (12 being the amount of pieces), and we're finally setting the entity's (`self`) `dead` flag to 1, so that it can be removed from our main entity linked list. If we now move over to fighters.c, we can see that we've made some updates and additions. To begin with, we've updated fighterDraw:
Before drawing we're testing the value of the fighter's new hitTimer variable. If it's 0, we're going to draw the fighter as normal. Otherwise, we're going to render the fighter with a red tint applied (via a called to SDL_SetTextureColorMod). This is a similar technique to that which we used in SDL2 Gunner and SDL2 Shooter 2. Now for fighterTakeDamage, a new function:
Pretty straightforward. We're subtracting from the fighter's `health` the value of `damage` we passed into the function, setting the hitTimer, and finally checking if the fighter has been killed (`health` is 0 or less). If so, and the fighter is not already dead, we're going to call the die function (for example, the one in greebleLightFighter.c). Now for bullets.c, where we've made an update and an addition. Before now, our bullets weren't checking for collisions; they had nothing to collide against. But now we have enemies, so we need to tell them how respond to that. standardTick has seen a very simple change:
We're now calling a function named checkCollisions. This, as we'll see next, is a function used to test bullet-entity collisions:
Another straightforward function. We're looping through all the entities in the stage, checking if their `side` is different to the bullet's `owner`'s `side` (i.e., this isn't an enemy-enemy collision or a player-player collision). Next, we're checking if the entity has the takeDamage function set, meaning it can be hurt. Finally, we're checking for a collision based on the bullet's rectangle and the entity's. If all of this is true, we'll add an effect to represent a hit (addHitEffect, defined in effects.s), and also call the entity's takeDamage function. We'll then set the bullet's `health` to 0 to remove it, and return out of the function, since we don't want to keep checking for collisions. All simply enough. Before bringing all of this together, let's quickly look at debris.c, to see how our debris works. Our debris is really nothing more than some effects, so we'll take a high level view of the code. First, initDebris:
We're setting up our linked list and setting a variable called fireTimer to 0. fireTimer, as we'll see, is used to control how often a piece of debris will release a fire effect. On to addDebris:
As we saw earlier, this function produces a set amount (`amount`) of debris, at a given position (`x`, `y`). In a loop, we're creating Debris and adding it to our linked list. We're then giving it a random velocity (`dx` and `dy`), a random thinkTime, and random `health`. The thinkTime of the debris is used to control how often it changes direction. Let's take a quick look at the doDebris function:
In this, decreasing the value of fireTimer, and then looping through all our debris. Each debris will call addDebrisEffect (in effects.c) if fireTimer is 0 or less. We're also decreasing the `health` of each debris, and moving it according to its velocity (`dx` and `dy`). We're then decreasing its thinkTime. If it's 0 or less, we're changing the `dx` and `dy`, and assigning a new thinkTime. This basically causes our debris to move about chaotically, changing direction every now and again, while streaming fire in its wake. That's all the main changes and updates done. We can now head over to stage.c, where we've put everything together (and added some new functions). Starting with initStage:
We're calling initDebris, and also setting a new variable called addEnemyTimer (static in stage.c) to 0. This variable will control the frequency at which enemies are added to the stage. Now for doStage:
We're doing several new things here. To begin with, we're setting Stage's numActiveEnemies to 0. As we've seen, when the enemy fighter we created calls its tick function, it will increment the value. What this means is that we'll always have an exact count of the number of enemies in the stage by the end of the doStage function. Next, we're reducing the value of addEnemyTimer. Once this hits 0, we're calling addEnemy, and resetting it to FPS (1 second). This means that once a second we will attempt to add a new enemy. Finally, let's look at addEnemy:
We're first testing how many enemies are active in the stage, and if it's fewer than 5, we're creating a new one, via a call to initEntity. We're then placing the newly created enemy within range of the player (it may appear on top of them, due to the nature of our random chance). It's important that we call this function after doEntities, so that we know how many enemies exist, otherwise this function will always add an enemy regardless of how many already exist, which isn't what we want. Excellent, part 3 is done! We're making steady progress in our game, and one can already see we've established a great framework for going forward. It's time to allow the enemies to chase after us and attack us (although we'll not allow them to destroy the player just yet). So, in our next part we'll implement some simple AI. 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 |