• 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 simple roguelike —
At some point, we've all played a roguelike. They really need no introduction. In this tutorial series, we'll look at how to create a simple one using SDL. We'll look at how to create a map, add enemies and combat, allow us to pick up items and weapons, and use and equip them. The crude plot of our little game is that a young lady working at a research lab has accidently let loose a hoard of mice, with microchips in their brains. They've made their way to the local dungeon (because in this world, those are things), and it's up to the researcher to deal with the little rascals before they cause chaos.
In this first part, we'll be generating a small random map to explore. Note that we'll be leveraging code from SDL2 Adventure, so there will be aspects of this tutorial that will be light on detail. If you want to know more, refer to the SDL2 Adventure tutorial.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./rogue01 to run the code. You will see a window open like the one above, showing our main character in a dungeon environment. Use WASD to move up, left, down, and right. The dungeon map will reveal itself as you move around. Notice how some parts of the dungeon become dark as you do so. This is to mark the visibility zones. We'll see this more in action in later parts. Once you're finished, close the window to exit.
Inspecting the code
As already said, we'll be using some code from SDL2 Adventure in this tutorial, mainly the map lighting and line of sight. To begin with, we'll start with defs.h and structs.h.
defs.h contains all our defines:
MAP_TILE_SIZE is the size of our map tiles, in pixels. MAP_WIDTH and MAP_HEIGHT is the width and height of our map (basically the number of tiles wide and tall it is). MAP_RENDER_WIDTH and MAP_RENDER_HEIGHT are how many tiles we want to draw horizontally and vertically. MAP_RENDER_X and MAP_RENDER_Y are the offsets of our map rendering. These are desirable because our screen size is 1600 x 900 and our map tiles, being 48 pixels, don't fit exactly (33.3333 x 18.75). We're therefore using a bit of maths to work out how to center the map, by using the screen resolution, and the width and height of the rendered map. TILE_HOLE, TILE_GROUND, and TILE_WALL are used to determine the type of tile. Note that our code will consider these values to be ranges, so a tile with a value of 1 - 49 is ground, while 50+ is a wall.
We've also got an enum with a couple of values:
ET is short for "entity type". Right now, we only have two - ET_UKNOWN and ET_PLAYER. ET_UKNOWN is just the default to say this is an undefined entity type, while ET_PLAYER will be used for the player.
Now, let's look at structs.h, starting with the good old Entity struct:
`id` is the unique id of this entity. This will come fully into play in a later part. `type` is the type of entity this is, such as ET_PLAYER. `name` is just the name of the entity, while `x` and `y` are its coordinates within the dungeon. `facing` is the direction the entity is facing, used for rendering, while `texture` is the image this entity is using for rendering. Our entities can form part of a linked list, hence the `next` field.
We also have a MapTile struct:
This struct will hold details of our map tile. `tile` is the actual tile number, used for rendering and type determination. `visible` will determine whether the tile is visible to us, from our position in the dungeon, while `revealed` will be used to say whether the tile has been uncovered by our exploration (and not covered by our fog of war).
Finally, we have our Dungeon struct:
This struct will hold the information about our dungeon (actually, our current dungeon floor). entityId is the next entity id to use when generating an entity. entityHead and entityTail are the entity linked list details. `player` is a pointer to the player's entity, useful for tracking the player for a number of reasons (such as displaying the hud later on). `map` is a multi-dimensional array of MapTiles, that will represent our dungeon floor. Note that it is MAP_WIDTH and MAP_HEIGHT in size, to cover the entire map. `camera` is a field to hold the current position of our camera, used for rendering.
Nothing difficult so far. Now, let's look at the something more exciting - the map. All our map code lives in map.c, including the map generation code. We'll start with initMap:
We simply call loadTiles here. loadTiles is a function you'll be familar with, if you've seen other tutorials in this series:
We're setting up a for-loop to load all our map tiles. We're going to load a maximum of MAX_TILES (defined as 100). When grabbing the tiles from our texture atlas, we're telling our getAtlasImage function not to error if the image doesn't exist, by passing 0 as the second parameter. With our tiles loaded, we're then loading an extra image called darkTile. This will be used to highlight a tile tha isn't in our current line of sight. We can see this in action now, as we come to drawMap:
Again, this is something we've seen in SDL2 Adventure. We're setting up two for-loops here, one to render on the horizontal axis and the other to render on the vertical, up to MAP_RENDER_WIDTH and MAP_RENDER_HEIGHT respectively. We want to work out which tiles to render according to our camera position, so we're adding the dungeon's camera's `x` and `y` to our current loop's `x` and `y`, and assigning these to variables named `mx` and `my`. We're then checking these variables fall within our map array range, before drawing.
We're then assigning the tile data at the map index to a pointer called `t`, to make things a bit more readable before continuing. What we're doing next is testing if the tile (`t`) in question's `revealed` flag is set. If it is, we'll be drawing the tile. If not, we'll continue with our loop. What this means, of course, is that any map tile that hasn't been revealed by our exploration won't be drawn. It will be effectively covered by our fog of war. When our tile is revealed, we'll first test it's not TILE_HOLE before drawing. After this, we'll test the tile's `visible` flag. If it's not set (0), we're render our darkTile image over the top of it. This means that any tile that isn't in our LOS will be shown darker than all the rest.
Okay, let's move onto the most interesting bit - the map generation. To start with, we have a function named generateMap:
This function delegates to two other functions, randomWalk and tidyWalls. We'll start with randomWalk (note - this function appears large, but is actually a bit more simple than that):
The idea behind the random walk function is to carve out a path within our dungeon, when starting with a map composed of nothing but walls. We'll first choose how much of the dungeon we want to convert from wall tiles into floor tiles, store this value as a counter, then choose a random direction to move (excluding diagonals). We'll move into that square and, if it's not already a ground tile, convert it into one. We'll then decrement the counter, to say we converted the tile. We'll continue to make these random movements until our counter hits 0. At some point, we'll also tell our random walk to move straight, to generate some tunnels. This straight walk will only last a few iterations before we return to moving in random directions. This will create a dungeon with areas and paths that are always accessible and never cut off from us.
Now that that's explained, we'll look at how we've implemented it in our code.
We're first declaring a variable called `coverage`, which will be used to determine the percentage of the map we want to convert into floor tiles. The value of this variable will be a random between 20 and 35. Next, we're calculating the total number of tiles we want to convert, by multiplying MAP_WDITH by MAP_HEIGHT, and then multiplying that by coverage, all finally multiplied by 0.01. In other words, we'll work out the percentage number, as a value between 0 and 1. So, if we have 100,000 tiles and a coverage of 20, we'll want to convert 20,000 tiles. We assign the number of tiles to convert to a variable called `n`.
Next, we're setting up two for-loops, `x` and `y`, to iterate across the entire map. For each map tile, we're memsetting the MapTile data, and then setting the `tile` field as TILE_WALL (plus a number of 3, to use a different pattern).
We then assign two fields called `x` and `y` a random point on our map (MAP_WIDTH for x, MAP_HEIGHT for y). In both cases, we're setting the minimum values to 1 and the maximum values to MAP_WIDTH / MAP_HEIGHT - 2. This will keep our starting points away from the edges of the map. We always want our map edges to be walls. We also set a variables called `dx`, `dy`, and `straight` to 0. `straight` is a variable that will control for how many steps our random walk will maintain the same direction. We then set the tile at our current `x` and `y` as TILE_GROUND, as this is our start position.
We then set up a while-loop, that will continue until `n` has fallen to 0 or less. Within this loop is where we'll perform our random walking. We first decrement `straight`, limiting it to 0. If `straight` is 0, we'll perform a switch against a random of 5. Depending on the outcome, we'll be updating `dx` and `dy`, or `straight`. `dx` and `dy` will be the horizontal and vertical directions we'll move in. Notice how we don't allow diagonals; we're only interested in moving straight up, down, left, or right. We'll do this for switch results of 0 to 3. If our switch result is 4, we'll be setting `straight` to a value of between 4 and 11.
We're now ready to move. We add our `dx` and `dy` to our `x` and `y`, limiting them to a minimum of 1 and MAP_WIDTH / MAP_HEIGHT - 2, to keep the new values away from the edges of the map, as before. We then test the value of the new map tile we've moved into. If the tile is a wall (>= TILE_WALL), we'll set the value of tile to TILE_GROUND. We'll also randomly add up to 5 to this value, to use a different ground tile texture. Finally, we'll decrement the value of `n`. Notice how we only want to decrease the value of `n` if we converted a wall tile into a ground tile. This is so that we can be sure that our coverage target is met. Our while-loop will continue until the value of `n` hits 0, meaning we've converted all the tiles we wanted to.
Finally, we set the value of the dungeon's player's `x` and `y` values to the current `x` and `y`. Basically, this is where our random walk ended up once we'd finished our map generation. This is just so we have a default starting place. In future, we'll pick somewhere more appropriate (such as next to a set of stairs).
And that's all there is to the random walk. A simple, but very effective map generation system. If we wanted larger open areas, we could increase our coverage value to something like 50% to 60%.
The next thing we want to do is tidy up the map a bit. The random walk can result in some stray walls, that look a bit untidy. We can do this with the tidyWalls function:
The goal behind this function is to find any wall tiles that are disconnected from the others. We'll iterate through all our tiles, looking for wall tiles, and count how many other wall tiles are next to them. If there are fewer than 2, we'll convert the wall tile into a ground tile.
We set up a do-loop, that will continue to cycle while we're removing walls. We'll set a variable called wallsRemoved to 0 at the top of the loop, to say we didn't remove any (as the default outcome). Next, we'll set up a for-loop, to copy all our existing dungeon map data. For our wall removal code to work best, we should work on a copy of the map data and modify that, rather than the actual map data. Not doing some can yield unexpected results. We've created a multi-dimensional array called `tmp`, the same size as our dungeon map, into which we've copied all the tile data.
With our copy made, we then loop through all our map data again, but this time keeping away from the edges, by 1. We then test the value of the dungeon map's tile at the `x` and `y` coordinates. If it's a wall, we'll call countWalls, passing in the `x` and `y` coordinates of the current tile. This function will return the number of walls that neighbour our current position. If there are fewer than 2 other wall tiles adjacent, we'll remove it. We set wallsRemoved to 1 and then set the value of `tmp` at `x` and `y` to TILE_GROUND.
Finally, we copy all the data from `tmp` back into our main dungeon map. If we removed a tile during our last pass, we'll repeat the loop again. This is because removing a wall tile may have resulted in another wall tile becoming orphaned. Typically, this loop might repeat 2 or 3 times before all the stray walls are dealt with.
The only other function to look at in map.c is countWalls. It's quite simple:
The function takes two parameters: `mx` and `my`, which represent the square we wish to test the neighbours for. We set a variable called `n` (number) to 0, then set up two for-loop, going from -1 to +1 (inclusive), on the horizontal (`x`) and and veritcal (`y`). The idea is to test the map tiles surrounding the current one, but exclude that tile itself. Therefore, so long as either `x` or `y` is not 0, and the tile at the dungeon's map `mx` + `x` and `my` + `y` is a wall, we'll increment `n`. Finally, we return the value of `n`, which will be the number of walls surrounding our current tile.
That's map.c done. We now have code to great a random map, that can be fully explored.
We should now move onto the other parts of the code. Starting with player.c. This file is quite easy to understand and contains all the logic for handling our player. Starting with initPlayer:
This function takes a single parameter - an entity. This entity will have been created in our entity factory (we'll see more on this in a bit). We set the `name` of the entity to "Player", the `type` to ET_PLAYER, grab the `texture` to use (gfx/entities/girl.png) and set the dungeon's `player` pointer as `e`. We also set a variable called moveDelay to 0. This variable will be used to control how quickly the player can move around the map.
The next function is doPlayer. There's not a great deal to it:
This function merely involves moving the player around. We start by decreasing moveDelay, limiting it to 0. If it's 0, we're ready to move, and set two variable, `dx` and `dy`, to 0. We're then testing our WASD controls, to see if any of those keys are pressed. If so, we're setting `dx` and `dy` as appropriate to the direction we want to move. With all our keys tested, we check if any movement has been performed (either `dx` or `dy` is not 0) and call moveEntity, passing over the dungeon's player pointer, as well as `dx` and `dy`, to move the player in that direction (we'll see what moveEntity does in a moment). We also reset moveDelay to MOVE_DELAY (defined as 5) and finally call updateFogOfWar, to uncover our map some more (again, more on this later).
With our player done, we can turn to entities.c, where all the entity processing is happening. entities.c isn't a large file, and only has three function right now. Starting with initEntities:
initEntities simply prepares the entity linked list by setting the entityHead as the entityTail. Moving on to moveEntity:
We saw this function earlier when handling the player. It takes three arguments: the entity to move (`e`), and the direction to move it (`dx` and `dy`). We start by adding together the entity's `x` and `dx`, and assigning it to a variable called `x`. When also add the entity's `y` and `dy`, and assign the result to `y`. Next, we check to see if `dx` is less than 0. If so, we'll face the entity left. If it's greater than 0, we'll face it right. Finally, we'll test that the `x` and `y` coordinates we wish to move to are valid. A tile is valid if it's within the map bounds, and the tile at those coordinates is a ground tile. Should this test pass, we'll set the entity's `x` and `y` as the values of the `x` and `y` we calculated.
The next function is drawEntities:
All we're doing here is looping through all our entities in the dungeon and drawing each one. We're assigning two variables, `x` and `y`, the values of our current entity's `x` and `y`, less the camera position, and multiplying these by MAP_TILE_SIZE. Remember that our entity's `x` and `y` are the positions on the map, so we need to convert them up to get our screen coordinates. With that done, we call blitAtlasImage and pass in the entity's `texture`; the calculated `x`, plus MAP_RENDER_X (the map render x offset) to correctly align it with the map; the calculated `y`, plus the map render y offset (MAP_RENDER_Y). We're testing which direction the entity is facing for the final parameter. If they are facing right, we won't bother to flip the texture (as our sprites all face right by default). Otherwise, we'll pass in SDL_FLIP_HORIZONTAL, to make the entity face right.
That's entities.c done. As seen a couple of times before, we have fog of war code, that will result in the map being slowly uncovered as we move around. The code for this lives in fogOfWar.c. We have just two functions: updateFogOfWar and hasLOS. As these have both been covered in the SDL2 Adventure tutorial, we'll only talk over them briefly. Starting with updateFogOfWar:
The first thing we do is set up two for-loops, to reset all the tiles in our map to be non-visible (`visible` = 0). We then set up two new for-loops, to handle the line of sight (LOS) for the player. Taking the player's position, we check an area of VIS_DISTANCE (defined as 16), and check all the squares within that distance, to see if the player can see them (using hasLOS). If so, we'll set the dungeon tile's `revealed` and `visible` flags to 1.
Our hasLOS function is Bresenham's line drawing routine, modified to handle line of sight checks:
We'll return 1 if we successfully reach the desired location, or 0 if we are blocked by a wall tile along the way.
We'll now briefly discuss our entity factory, something we introduced into SDL2 Gunner and SDL2 Adventure. All the code lives in entityFactory.c. Starting with initEntityFactory:
We set our initFunc linked list by memsetting the head of the chain (`head`) and pointing the end (`tail`) at the head. Next, we call addInitFunc, passing in "Player" and initPlayer, to setup a player init function.
initEntity is something we'll see a lot in this tutorial. It works just the same as it did in the past tutorials:
We'll call spawnEntity to create an entity and then look for an initFunc with a name matching the one we passed into the function, to set everything up. After that, we'll return the entity.
Our spawnEntity function won't come as a shock to anyone:
We're mallocing and memsetting an entity, and adding it to the dungeon's entity linked list. One thing we are doing extra is setting the `id` of the entity. We're grabbing the next number from dungeon's entityId variable. We first increment dungeon's entityId and then assign it to the Entity's `id` field. While we're not using this id just yet, this is something we'll be using later on in the tutorial when it comes to loading and saving. There's no harm in doing this now, however.
That's all the core logic for this part done. We need now only pull it all together, to make the game playable. All our main loop code lives in dungeon.c. There's a few functions here, so we'll go from the top. Starting with initDungeon:
We're memsetting our dungeon, to zero all its memory, then calling initMap to prepare our map data, and then calling off to another function named createDungeon, to set up the dungeon proper. We're also assigning our logic and draw delegates to the `logic` and `draw` functions in dungeon.c.
Our createDungeon function is simple enough, it's just delegates to other functions right now:
We're calling initEntities to setup our entity data, and then calling initEntity, with "Player" as an argument, to create the player. We then call generateMap to create our map, and updateFogOfWar to reveal the map around the player. Calling updateFogOfWar here is important, so that the player isn't standing in complete darkness right away.
`logic` is next, and once again there's not a lot to it:
We're just calling doPlayer and doCamera. doCamera is a function that handles our game's camera logic:
We want our view to be centered around the player at all times, and so we take our player's current position and subtract half of the map's render values from the player's `x` and `y`, to shift things into the middle, and assign these to the dungeon camera's `x` and `y`.
Our final function is `draw`:
Again, we're just delegating the calls, calling drawMap and then drawEntities.
We're almost done. The only things left to do is setup the game system. In init.c, we have initGameSystem:
We're seeding our random with a call to srand, using the current time, and then calling initAtlas and initEntityFactory, to setup our texture atlas and entity factory.
Our main function in main.c is last:
We just need to call initGameSystem and initDungeon, to get things going (along with the standard unending while-loop to receive input, handle logic, and draw everything).
That's it for the first part. There was quite a lot to start with, but we've laid the foundations for creating a roguelike. We can generate a random map and move around in it, as well as reveal the squares and process our line of sight data. In the next part, we'll look at introducing a monster (one of the mice). It will be static and not do a lot, but we'll be taking baby steps.
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: