![]() | |
PC Games
• Orb Tutorials
• 2D shoot 'em up Latest Updates
SDL2 Quest game tutorial
SDL2 Versus game tutorial
Download keys for SDL2 tutorials on itch.io
The Legend of Edgar 1.37
SDL2 Santa game tutorial 🎅
Tags • android (3) • battle-for-the-solar-system (10) • blob-wars (10) • brexit (1) • code (6) • edgar (9) • games (44) • lasagne-monsters (1) • making-of (5) • match3 (1) • numberblocksonline (1) • orb (2) • site (1) • tanx (4) • three-guys (3) • three-guys-apocalypse (3) • tutorials (18) • water-closet (4) Books ![]() The Honour of the Knights (Second Edition) (Battle for the Solar System, #1) When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and the Mitikas Empire's civil war than the Helios Confederation is willing to let on. Somewhere out there the Pandoran army is gathering, preparing to bring ruin to all the galaxy... |
— Simple 2D quest game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction Now that we have our world and islands generated, it's about time we added the ability to explore the world. In this part, we'll be adding in our robot adventurer, who will be able to walk around on land, to see what it has to offer. As our robot moves from square to square in a single jump, this will be very easy to do (we've already seen it done in SDL2 Adventure and SDL2 Rogue). Our robot won't be able to move into sea, forest, or mountain tiles. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest04 to run the code. As before, you can move around with the WASD control scheme, only this time you will be controlling the robot. The camera will track your movement as you explore. Pressing Tab will open the Quest Log, so you can get an idea of where you are (you will be placed on the largest island). However, your exact location is currently not displayed. When you're finished, close the window to exit. Inspecting the code If you've been working through these tutorials (or have at least investigated SDL2 Adventure / SDL2 Rogue), what follows will be quite familiar. As we've touched on things like entity creation in the past, we'll not be looking at anything to do with entity management and processing, and instead just the parts that are important to our quest game. So, let's make a start with the code updates. First up, we have the Entity struct:
There's not a lot to say about this, as we've seen it before. One thing to note is the inclusion of an `id`. Each entity that is created will be given a unique, incrementing id. This is to enable them to be identified wherever they are in the world. The entity factory is responsible for setting and incrementing this id, whenever we create a new Entity (via initEntity). Next up, we've updated the Map struct:
We've added in support for our entity linked list (entityHead and entityTail). Again, be aware that the overworld and towns will all have separate maps, and therefore will have their own collections of entities. This is partly why we need the unique id field in the Entity struct, so that we can find an entity by its id, anywhere; we don't want two entities to exist in our game both having an id of 0, for example. With that done, we can look at how our player is added to the world. If we head over to overworldGen.c, we've can see a few tweaks have been made. First up, we've updated generateOverworldWorker:
We're making a call to a new function called addAdventurer:
This function is responsible for adding our robot adventurer to the world. When we add in our robot, we want to place them on the largest island in the world. We do this simply by looping through all the islands, finding the one with the greatest size, and setting that as our starting Island (as startingIsland). With the island found, we then create the Adventurer entity, and attempt to randomly place it somewhere on the island. We do this by simply selecting a random point (as `x` and `y`) within the island's bounding box, and checking to see it is ok to place the player there. So long as there are no other entities occupying that space, and the map point is within the range of shallows (MT_SHALLOWS) and normal land (MT_LAND), we can set the player location (again, the player isn't allowed to cross the sea, forests, or mountains, so we have to ensure that we start them in a location where they can walk). With that location set, we set the current Map's `player` pointer as the created adventurer (`e`). Each map will have its own adventurer entity; we don't teleport the player object around themselves, since our game doesn't require us to do such a thing (in SDL2 Rogue, we did this, as we needed to retain the entity's state - in this game, we can get away with multiple player instances). It also save on micromanaging the player state. With that done, we can now move over to player.c (in the game directory). This is a new compilation unit that will handle the player themselves, dealing with the input, movement, and such. Starting with initPlayer:
We're setting up two variables here, moveTimer and `speed`. moveTimer will be used to add a delay between movements, as we saw with the camera. `speed`, on the other hand, will determine the reset value of moveTimer whenever we move. We're setting this as a variable, rather than a constant, so that we can allow the player to accelerate when they walk around. This is done in order to give the user more control over the movement. When we first move the robot, the movement will be slow, but will get faster the longer we hold down the movement controls. This will help later, when we want to align ourselves with objects in the world; moving too fast will make this quite frustrating! Now over to doPlayer, which is the function that will handle controlling the player:
This is a pretty standard movement handling function, testing the state of our WASD control scheme, to see how we should move. If movement requested, we'll delegate to the `move` function, passing over `dx` and `dy`. Note that we're handling `dx` and `dy` separately, so that we can slide off walls, etc. Once the player has moved, we'll set moveTimer to the value of `speed`. Next, we'll decrease the value of `speed`, so that the next movement will happen faster. We're capping this to MAX_SPEED (defined at the top of player.c). If no movement is detected, we'll set `speed` back to MIN_SPEED. Next up, we have the `move` function itself:
Right now, we're calling two other functions: moveEntity and moveOverworld. What we're doing here is first checking for movements against other entities. If there aren't any entity interactions (that could block the player movement), we'll call moveOverworld. We'll quickly look at moveEntity next:
As expected, we're testing if there is an entity at the location the player is about to enter (`x`, `y`). If so, we'll call the other entity's `touch` function (to invoke that entity's interaction against the player). We'll then return whether the entity is `solid`. If it is, the call to moveOverworld in move won't occur, since this entity is occupying the space we wish to move to. Otherwise, we'll return 0. Nothing out of the ordinary, then. Finally, we have moveOverworld:
Another pretty standard function - this simply deals with moving the player against the world. Notice something, though - when considering the tile type the player wants to enter (`t`), we're dividing the map data point by TILE_TYPE_RANGE. This is so we can reduce values of 1, 6, 14, 15, 43, 48, etc. down to 0, 1, 4, etc., thus matching our MT_ enum. Remember that we decorated the overworld earlier, increasing the value of the tiles at the map points by 10, and adding an additional value up to 9. We therefore need to reverse this to check the tile type. And that's really all we need in order to insert our player into the game. As we finish up, we'll look at the updates we've made elsewhere to support it. First, to overworld.c, and initOverworld:
We're calling initEntities and initPlayer. Next, the update to `logic`:
We're calling doPlayer and doEntities. Lastly, we've updated `draw`:
We're calling drawEntities here now, after we render the map. A tweak to doCamera in camera.c was also required, to track the player:
Pretty standard stuff, with the camera being centered over the player. Before we conclude, remember that our entity handling will use the context of the current Map, in game. If we take a look at the getEntityAt function, we can see this:
We're checking the entities in the current map (Game's `map` is a pointer). This will be the case when we call doEntities and drawEntities, as well as various other entity handling functions. We'll see how this is put to further use when we get to generating towns. It essentially means we're working with a state machine. For now, Game's `map` will always point at the Overworld. Another part done. But we're stuck on a single island, with no means of reaching the others. What we need is a boat, to cross the vast seas. This is something we'll add into our next part. Since we already have entity interactions in place, this will be very easy to do. We'll also be stranding the player on the smallest island, as an incentive to get out and explore (they'll also need to find the boat!). 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 |