PC Games
• Orb Tutorials
• 2D shoot 'em up Latest Updates
SDL2 Versus game tutorial
Download keys for SDL2 tutorials on itch.io
The Legend of Edgar 1.37
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
Tags • android (3) • battle-for-the-solar-system (10) • blob-wars (10) • brexit (1) • code (6) • edgar (9) • games (43) • lasagne-monsters (1) • making-of (5) • match3 (1) • numberblocksonline (1) • orb (2) • site (1) • tanx (4) • three-guys (3) • three-guys-apocalypse (3) • tutorials (17) • water-closet (4) Books Firmware HF-Tech's chips have changed the world. Embedded into the heads of over 90% of the world's population, they have cured autism, dementia, provided intelligence boosts, and helped to ease some of the more mundane tasks in life. Daniel Blair, hacker and Workshop member, however is not convinced that everything is as rosy as it seems. But is he looking in all the wrong places..? |
— Making a 2D split screen game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction Now that we have our split screen in place, we should start looking into implementing our gameplay loop. As there is quite a lot to this, we'll be doing it over several steps. This step will involve handling the player's ship being destroyed, and the subsequent respawning. Up until now, our players have been completely invulnerable; being shot, or colliding with the environment or other player does nothing but results in the ship bouncing away. Now, the player's ship can be destroyed. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus06 to run the code. You will see a window open like the one above, with each player on either side of our zone. Use the default controls (or, at your option, copy the config.json file from a previous tutorial, to use that - remember to exit the game before copying the replacement file). Play the game as normal. Notice how constantly flying into the ground or against the other player will eventually lead to one (or both) of the craft being destroyed. After a short time, the player will reappear in their starting position, with a protective shield. Once you're finished, close the window to exit. Inspecting the code Adding our in respawning requires us to make a number of updates throughout the code, but isn't a difficult task to accomplish overall. Starting first with defs.h:
We've added in a define called MAX_PLAYER_HEALTH, that will be the maximum value of the players' health. Notice that it is a double, rather than an int. This is because we want collisions with the environment to cause damage over time, so we aren't tied to our frame rate. Next, we turn to structs.h, where we've updated Entity:
We've made a bunch of additions and changes to this struct. The new `dead` flag defines whether an entity is alive or dead. respawnInterval, respawnTimer, and respawnPosition all govern this entity's respawning state. respawnInterval will determine how long before the entity is re-created upon death. respawnTimer is used to control this countdown until it is respawned, while respawnPosition holds the position where the entity is to reappear. We've also added in function pointers called takeDamage and `respawn`. takeDamage will be invoked whenever this entity takes damage, `respawn` when it is re-created. We'll see these in use when we come to the updates to player.c. Next up, we've made changes to Player:
We've added in `health` and `shield` variables. As their names suggest, these variables represent the Player's current health and shield amounts. Finally, we've updated Zone:
We've added in an entity here called WORLD. This will be used whenever an entity takes damage from the environment. As we'll see, this helps to keep our takeDamage function more fluid, since we can avoid a NULL check. Now for the changes to player.c, starting with initPlayer:
When we create the player, we're now setting the entity's respawnInterval to two seconds, and its respawnPosition to the same position as where the player is first located. We're also setting its takeDamage and `respawn` function pointers (to function with the same names). With that done, we're calling the `respawn` (we'll see this in a bit), and setting the Player's `shield` to 0. The reason for this is because our `respawn` function sets the player's `shield` to 5 seconds, but we don't want our player to have a shield when the game first starts. Next, we can look at the changes to `tick`:
A one line addition - we're decreasing the value of the player's `shield`, limiting it to 0. This will, of course, mean that our shield will expire after a period of time. Now, over to `draw`, where we've made a larger update:
Here, we're drawing the player's model as usual, but after that we're testing to see if the player has a shield. If so, we're going to render it as a hexagon. To achieve this, we're using a series of SDL_Points, along with the SDL_RenderDrawLines function. The calculated points are positioned around the player, with the location of the camera also taken into account. There is scope for optimisation here, since we're calculating the corners of the hexagon all the time, when we could do this once, and just adjust the position. This would be a micro optimisation in our game, however, and likely wouldn't result in any noticeable impact to performance. Now over to `touch`, and here you'll notice we have a much larger update:
Here, you can see why we have a "WORLD" entity in our Zone struct - we're expecting `other` never to be NULL. We first test the `type` of the entity. If the `type` is "ET_NONE", we'll consider it to be the world / environment. We'll first calculate how fast the player was moving when they made contact with the environment (according their `dir`), and multiply that by 5, to determine the amount of damage to apply (as `damage`). We'll then call takeDamage, passing over the entity, the amount of damage to apply, and the entity itself (`self`) as the attacker (3rd argument). Finally, we'll call `bounce`, to push the player back. In effect, this means that flying into the ground will harm the player. And the faster they go, the greater the damage will be. If `other` is another player, we'll once again calculate how much damage to cause (but this time multiply by 2.5). We'll then damage BOTH the current player and the other player, with the current player being the attacker. We'll then adjust the other player's `dir` by half the current player's `dir`, in effect pushing the opponent. Finally, we'll once again call `bounce`, to push the current player back. This means the players can shove each other about a bit. That's it for our updates to `touch`. Let's now look at our new takeDamage function:
This function takes three arguments - the current entity, the amount of damage to apply, and the attacker. The attacker isn't actually used at the moment; it's here now so that we don't need to make changes to the function signature later. The function itself is rather basic. We test if the player has a shield, and if not we're subtracting `amount` from the player's `health` (limiting to 0). The player's `dead` flag is set to 0 if their `health` has hit 0. So, a standard damage function. Finally, we come to the `respawn` function:
This function basically resets the player's state. We set their `angle` to 0, and reset their `dir` (to stop them moving). The player's `health` is set to MAX_PLAYER_HEALTH, and we give them a shield that last 5 seconds. This is why in the initPlayer function we're setting this value back to 0. That's it for player.c. We can now turn our attention to entities.c, to see how our respawning logic works. It's not difficult to understand. Let's start with initEntities:
We're setting up a new linked list here, to handle our respawnable entities (as respawnHead / respawnTail). The reason we're doing this is to separate our living entities (that we'll want to render, collide with, etc.) from the ones that are waiting to return to the game. Doing this helps to do away with micromanagement (testing if an entity is dead before drawing it, etc) and strange bugs that could occur elsewhere. With that done, we can implement it in doEntities:
The major change here is that we're now testing the entity's `dead` flag. If the entity is dead, we'll want to either remove it from the game completely, or add it to the respawn list. In both cases, we start by removing it from Zone's entity list. Next, we check if the entity's respawnInternal is greater than 0 (meaning it will respawn). If so, we'll set its respawnTimer to the value of its respawnInterval, and then add it to the respawn list (setting respawnTail's `next` to the current entity). If the entity isn't flagged for respawnnig, we'll simply delete it from our game. That should all be quite clear. In summary, if a respawnable entity is killed, we'll move it to the respawn list, rather the remove it completely. A minor update to touchWorld follows:
As stated before, our `touch` functions expect a non-NULL entity to be passed over, so the `type` can be tested. Here, we're changing our once NULL parameter to be Zone's WORLD. Finally, we've added in a new function for handling our respawnable entities - respawnEntities:
This function is responsible for bringing our entities "back to life", and returning them to the game world. We simply loop through all the entities in the respawn list, and reduce their respawnTimer value. If it hits 0 or less, we'll remove it from the respawn list, set its `dead` flag to 0, return it to its starting position (respawnPosition), call its `respawn` function if one is set (as it is, for our Players), and finally return the entity to Zone's entity list. So, almost the reverse of when an entity dies, except that we're moving it back into the world, and resetting its various attributes. There are, of course, a number of different ways we could've approached this, but this one saves us from a bit of micromanagement. Finally, we just need a call this new function, so it's over to zone.c, where we've updated `logic`:
We're now calling the respawnEntities function. We're finished! We can now cause damage to our players, they can die, and be reborn. That's just what we need in our game. However, it's not too useful that we can't see how much health, etc. the players have. In fact, we can't see much of anything; there is no status display to be found anywhere in our game. So, in our next part we'll look at introducing our good old friend, the HUD. Purchase The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle: From itch.io |