![]() | |
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 In this tutorial series, we're going to look at how to create a simple 2D quest game, where the player can explore a randomly generated world, hop between islands, visit towns, talk to the residents, and take on quests. Quite a lot! Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./quest01 to run the code. You will see a window open, displaying the overworld view. The view will be centred in the middle of the map. Due to the nature of the random generation, this could be over land, sea, or anywhere else. Use the WASD control scheme to move around. Press Tab to open and close the Quest Log, which will show the full map (like the screenshot above). When you're finished, close the window to exit. Inspecting the code As we're now very far along in these tutorials, we'll be skipping over anything that has already been covered and seen multiple times, such as entity factories, sound, and all the rest. Some aspects will also be cloned from previous work. We'll refer back to the relevant tutorials and parts as needed, but otherwise won't be going into depth. This will allow us to focus on what makes this game different, and how the generation, rendering, etc. works. In this first part, we're going to look at generating the world map itself. To do this, we'll be generating data using a Perlin noise implementation. Let's first look at structs.h, where we've created our data structures. Starting with Map:
This struct will hold the data about a map. We're going to have several maps in our game - one for the overworld, and one for each of the towns. As a result, we'll be storing the size of the map itself, along with field holding the data. Each map will also have its own `camera` (as an SDL_Point). When it comes to creating the map, we'll allocate a multi-dimensional array of ints, of size `width` and `height`, and store them in the `data` field. Next, we have the Overworld struct:
This will hold all the information about the overworld. For now, it's quite simple. `seed` is the seed used to create the world, `map` is the map data, as we saw earlier, and `miniMapTexture` will be used to hold the overview texture we will generate later (again, this is what is displayed in the Quest Log, and what will show up in the HUD). Finally, we have the Game struct:
Again, not a lot here right now; we're storing an Overworld object, as well as a pointer to the current Map. Since we'll be moving between the overworld and towns, this `map` pointer will act as context, so we'll know the constraints of the current map, as well as the entities contained within it. We'll see this expanded out in future. Now, let's turn to the actual map generation. This code will all live in a directory called gen, so that it is separated from the game logic:
. ├── generating.c ├── generating.h ├── overworldGen.c ├── overworldGen.h ├── perlin.c └── perlin.h There are a number of C files here, but there's really only one we're interested in - overworldGen.c. generating.c is where we throw up a screen to show the player that something is happening. This basically operates in the same way as the generating screen found in SDL2 Strategy. perlin.c is an implementation of Perlin noise generation. Its job is simply to generate a Perlin noise map of a given size. We'll be skipping over this, too. Therefore, our focus will purely be on overworldGen.c. Let's start with `generateOverworld`:
This just sets up the background thread to generate the map. Right now, our generation doesn't take very long, but in future it could take a few seconds. The thread uses the function generateOverworldWorker:
Right now, this function is pretty straight forward; it will be expanded in future to do much more, such as creating towns, etc. For now, it's only concerned with generating the map data itself. We start by calling generatePerlinNoise (done in perlin.c). This function returns a 2D array of doubles, with values between 0 and 1. We could use any noise function we like here, so long as it returns the data in the format that we are expecting. We store this returned data as `elevation`. Next, we'll prepare the overworld's map, by setting its `width` and `height` (to WORLD_WIDTH and WORLD_HEIGHT, both defined as 512), and allocating a 2D array to hold the data (as an array of ints). As we do this, we'll set a variable called `maxElevation` to the largest value in the noise map. This will help in the next part when we come to determine the data thresholds. With our elevation data available and our overworld map setup, we then loop through our noise data, and work out the type of tile will exist in the overworld map, based on the elevation at the same point. Note that we're dividing the elevation data by maxElevation, to determine a percentage, and produce more consistent results. For our maps, anything belong 60% is water, anything below 61% is swallows, below 90% is normal grassland, and above that is mountains. You're likely wondering where these numbers come from, but the truth is that there are no hard and fast rules attached to them; you can fiddle these numbers as you like, to suit the world you wish to generate. The numbers we're using here aim to create a world with a large sea, and about 30% land. It's good to experiment with the thresholds. We could increase the shallows, for example, by changing 0.61 to 0.62 (although, this might be far too big a change!). Have a play, and see how it turns out for you. With that done, we clean up our noise data, and call worldGenDone, to invoke the callback that our world generation is finished. The thread then exits. So, in summary we have created a Perlin noise map, and used that to determine the type of tile that will exist on our overworld map. 0.6 acts as our water level, while above that we get land and mountains. Pretty simple, yet with great results. For those curious, below is what our raw Perlin noise data might look like (it's random each time):
That's our land generation done, so we'll take a look now at overworld.c, which lives in the game directory. This is where we'll render and handle the overworld view, when the player is traversing the world. There won't be a large number of surprises here. First up, we have initOverworld:
We're setting the `seed` of the overworld, and then using that same value as the seed for the srand function, to generate a unique world each time. We setting Game's `map` context to be the overworld's map, and then positioning the camera in the middle of the world. Finally, we're loading our graphics tiles, and setting a flag call doDecorate to 1. This flag is used to add some variety to the world, as we'll see shortly. Next, we have the enterOverworld function:
This function will be called in a number of different places, such as when the world generation is finished, when we exit the Quest Log, and also when we exit a town. We're first checking the doDecorate flag, to see if we need to add decoration to our world, and then setting Game's `map` context. We're also setting the `logic` and `render` delegates. Nothing taxing. Note that we're only going to call doDecorate once, since we're clearing the flag right away. The `logic` function follows, and is also quite basic:
If we press Tab, we'll jump into the Quest Log (we'll see more on this in the future). Note that we're passing over enterOverworld, which is the function the Quest Log will invoke when we exit it. This is important, as we need to know whether to return to processing the overworld or a town (or other environments in future..!). We're also calling doCamera (done in camera.c), to handle our camera controls. Finally, let's look at that `decorate` function we mentioned earlier:
What this method does is updates the value of the overworld's map data, at each point. You will remember that we set the values of the data as MT_WATER, MT_SHALLOWS, etc. These are values within an enum (in defs.h), and start from 0. If our tiles matched these values, we'd simply render tile 0, tile 1, etc. However, we want our water, shallows, and grass tiles to vary. As such, we're going to space them 10 apart. So, water tiles can be 0 to 9, shallows 10 - 19, lands 40 - 49. To acheive this, we first multiply the existing value by 10, so that we have values 0, 10, 20, 30, etc. Next, we randomly add an amount based on TILE_TYPE_RANGE (10), and assign this to a variable called `n`. At this point, `n` will hold a value such as 3, 7, 13, 18, 41, 47, etc. We check to see if we have a tile at that index and if so, we assign it to the overworld's map data. In short: we know that tiles 0, 10, 20, 30, 40, etc. will exist in our tileset, so this is the default. We then try and randomly choose another tile within the type range to use, to add variation. If such a tile exists, we'll use it. Otherwise, we'll stick with the default. This works well to give the world different tiles for land and sea, and breaks up the monotony. That's mostly it for this first part. Just for completeness, let's look at the map and camera code. Starting with drawMap in map.c:
This function accepts an array of tiles (as AtlasImages) as an argument, which it will use to pick from during the rendering operation. This is helpful for when we want to either render the overworld or the town. Note also that the function is using Game's `map` in the context of the drawing. Otherwise, this is a standard map rendering function that we've come to know and love. If we also consider the screenshot below of the world map rendering:
You can see how the decorate function has randomly chosen a grass, shallows, or water tile from the ones available in the texture atlas. Finally, let's look at the doCamera function (in camera.c):
doCamera simply allows us to scroll the map around, using the WASD control scheme. This is just a temporary piece of logic; once we have implemented the player character, the camera will track them as they move around. And that's it for this first part. As you can see, creating this world is pretty simple. We just need a good noise generation system, and then we can treat the values as an elevation map, to help us decide what is water, what is land, and everything else. But you've probably noticed we don't exactly have an island system. It would be preferrable to have an archipelago, or at least one large land mass, that doesn't touch the borders of our world. This will give us a chance to use the boats that we will be adding later. Some extra tile types would be nice, too. What about forests, snow-capped mountains, and desert areas? Well, in the next part, we'll look at implementing these missing features. 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 |