• 2D shoot 'em up
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
TBFTSS: The Pandoran War - Amiga OS4 Port
— Creating a Run and Gun game —
We're closing in on having the main framework for our game finished. We just need to make a few more gameplay tweaks in order to make it complete. This part will feature the first set of gameplay tweaks, with the others coming in the next part, along with effects such as explosions and handling the player death.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./gunner11 to run the code. You will see a window open like the one above. The same controls from past tutorials apply. There are just three enemies on this level, to demonstrate the gameplay changes. Notice how the blue soldier is unable to see the player until both doors have been opened and a clear line of sight has been established. Also notice how the second green soldier doesn't fire if his buddy is blocking his LOS when he sees the player. When you're finished, close the window to exit.
Inspecting the code
We've made a number of tweaks to a number of files to support our initial batch of gameplay changes. We'll start with defs.h:
We've introduced a new flag called EF_BLOCKS_LOS. This flag, when applied to an entity, will tell our line of sight code that this entity blocks line of sight. This can be applied to non-solid entities, such as enemies. We've also added in a new enum:
This enum will be used to determine which "layer" our entities should be drawn, as either the foreground (0 - default) or background (1). If we now look at structs.h, we can see that we've added in a new field to Entity to support this:
`layer` is the only field we need here. Everything else remains the same.
Now let's have a look at how we're going to use our new enum. You'll remember that doors in the previous parts would raise when opened, but would cover the map. It would look better if they were behind the map when they opened. Turning to doors.c, we've made one minor change to start supporting this. Looking at initDoor:
We've told our door entity that it's `layer` is LAYER_BACKGROUND. And that's all we need to do here.
If we now turn to entities.c, we've made a change to drawEnitites:
Our drawEntities function now take a parameter called `layer`. We're using this parameter when we're looping through all our entities to draw, by testing if the entity's `layer` is the same as the `layer` we passed in. In other words, if we pass in LAYER_BACKGROUND, we'll only draw the entities whose `layer` is also LAYER_BACKGROUND. If the parameter is LAYER_FOREGROUND, we'll only draw the entities whose `layer` is also LAYER_FOREGROUND.
Now, we turn to stage.c, we've updated `draw`:
Now, instead of calling drawMap and then drawEntities, we're first calling drawEntities with LAYER_BACKGROUND, then drawMap, then drawEntities with LAYER_FOREGROUND. In effect, our doors will be rendered first, then the map, then our other entities. This means our doors will now render behind the map. Of course, we can expand this code to other entities if we wish, not just doors. You will have noticed that there is a chance for some optimisation here, as we're searching the quadtree twice for the entities to draw. This is something we could tweak in our final part.
Another gameplay aspect we want to look into is that enemies can shoot and kill one another. Fixing this issue is very easy. If we turn to greenSoldier.c, we need only update the takeDamage function:
Our takeDamage function takes a parameter called `attacker`, to say who inflicted the damage. To prevent enemies from harming one another, we need only test if the `attacker` is the player (`stage.player`). If it is, the damage code and response will proceed as normal. Otherwise, nothing will happen. Note that by design, the bullet that hit the enemy will be absorbed and vanish (see bullets.c). The above has been done for both greenSoldier.c and blueSoldier.c.
Another gameplay fix we want to make is to prevent enemies from shooting backwards. If an enemy starts firing, they will remain facing the same direction, even if the player moves past them. To fix this, we can make a small adjustment to `tick`:
Now, before firing, an enemy will check which way they need to face. We do this by testing the player's `x` against the enemy's `x`. If the player's `x` is greater than the enemy's `x`, the player is to their right. We therefore assign the enemy's `facing` to FACING_RIGHT. Otherwise, we'll set it to FACING_LEFT.
Another minor change we want to make is to damage the player if they run into an enemy. This, again, is simple. Our green and blue soldiers currently don't implement a `touch` function. We can therefore define one as such:
We're just checking what touched the enemy (`self`). If it's the player (`other`), we'll call takeDamage for `other`, passing in the player, 1 point of damage, and the enemy itself. And that's all we need to do. Now, when a player runs into an enemy, they'll take 1 point of damage.
Sticking with the enemies for the moment, we can look into how we're using our new EF_BLOCKS_LOS flag. Taking initGreenSoldier as an example:
We're setting the entity's `flags` to EF_BLOCKS_LOS to tell our code that the enemy soldier will block the line of sight of others.
To see how this works, let's take a look at ai.c, starting with canSeePlayer:
canSeePlayer is called by enemies, to check if they have a clear line of sight to the player. Before, we were just calling traceMap. Now, we're also calling a new function named traceEntities:
The idea behind this function is to test whether there are any entities between the observer (the enemy) and the player. The function will make use of line-rectangle collision detection, testing whether a line drawn from the enemy to the player intersets with either anything solid (or flagged as blocking) on the way.
To start with, we're defining several variables. `sx` and `sy` are the starting x and starting y of the line. We're using the entity's `x` position, plus half their texture width as `sx`, and their `y` value as `sy`. `ex` and `ey` are the ending x and y of the line. We're defining these as the player's `x` plus half their texture width as `ex`, and the player's `y` as `ey`.
With our line's start and end point defined, we then want to collect all entities that might be crossed by it. We going to search our quadtree to grab the entities, but we require a rectangular area for our getAllEntsWithin function. We can find this easily by determining the top-left and bottom-right of our line, by using MIN and MAX macros with our `sx`, `sy`, `ex`, and `ey` variables. The results are assigned to `x1`, `y1`, `x2`, and `y2`, giving us the corners of our rectangle. We then pass these into getAllEntsWithin, as well as an array to hold the entities (`candidates`), while telling the function to ignore the observer (`e`).
Next, for each of the entities that are returned, we're testing first the entity's `flags`. We're only interested in entities that are solid (EF_SOLID) or block the line of sight (EF_BLOCKS_LOS). If so, we're calling a new function named lineRectCollision. This function tests whether a line intersects with a rectangle. We'll see more on this in a little while. Should both these conditions hold true, we're going to return 0 (false) to say that the line of sight has been blocked. However, if we process all our entities and not find any entities interferring with our line of sight, we'll return 1.
Of note is that we've updated traceMap to also use this line-rectangle collision detection, as it's a bit more accurate:
It mostly works in much the same way as traceEntities, in the rectangular area to test. With this done, we're dividing `x1`, `y1`, `x2`, `y2` by MAP_TILE_SIZE to find the area of the map to test, assigning these to `mx1`, `my1`, `mx2`, and `my2`. We're then looping through the tiles in this area. The LOS is considered blocked if the tile indexes are inside the map (isInsideMap) the map tile is a non-zero value, and a line-rectangle intersection (lineRectCollision) occurs in the area occupied by the map tile. Again, if this happens we'll return 0. If we reach the end of the function without an intersection occuring, we'll return 1.
That's this part almost done. Before finishing up, we'll look at the lineRectCollision function that lives in util.c:
To test our line-rectangle collisions, we're actually testing our line (`x1`, `y1`, `x2`, `y2`) against all four sides of our rectangle (`rx`, `ry`, `rw`, `rh`). For this purpose, we're calling another function named lineLineCollision. We'll test first the top of the rectangle, then the bottom, then the left, then the right. If our line intersects any of these lines, we'll consider that the line has intersected the rectangle.
The line-line collision test is not something we'll discuss, as it's one of many different implementations that has proven to meet our needs. If you're interested, the implementation can be seen below:
That's some of the outstanding gameplay tweaks done. We've now only got to handle the player's death and add in some effects (explosions, weapon impacts, etc), and then we can move onto creating a full level.
The source code for all parts of this tutorial (including assets) is available for purchase:
It is also available as part of the SDL2 tutorial bundle: