• 2D shoot 'em up
The Legend of Edgar 1.37
SDL2 Santa game tutorial 🎅
SDL2 Shooter 3 tutorial
The Legend of Edgar 1.36
SDL2 map editor tutorial [UPDATED]
— 2D Santa game —
It's the most wonderful time of the year! If it's December, and you live in a country that celebrates Christmas, that is! In this tutorial series, we'll be looking at how to create an infinite scrolling game, where the player takes on the role of Santa Clause, in his bid to deliver as many gifts as possible on Christmas Eve. We'll discuss the naturally absurd plot of our game at a later point. For now, we'll look into getting the basics working.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa01 to run the code. You will see a window open like the one above, with the ground moving from right to left. There's not much more to see or do here, so when you're finished, close the window to exit.
Inspecting the code
In this first part, we're going to be implementing our infinite scrolling ground plane, using a tile map approach. You'll find it's quite easy to do. None of these parts are going to be super detailed, as by now the pattern of our game development has been well established, and so we'll focus on the things that make SDL2 Santa the game it is.
We'll start with defs.h:
We're setting us two defines here. GROUND_TILE_SIZE is the width and height of our ground tile in pixels, while GROUND_Y is the vertical position of our ground plain. For its value, we're just subtracting the size of our ground tile from the height of the screen, so the ground is pinned to the bottom.
Now over to structs.h. We've only got one major struct to consider here:
Our Stage struct holds just the horizontal speed of our scrolling. We're adding this here as we'll want our game to speed up over time, and so entities and other things in the game will need to know how fast we're moving.
Nothing major so far. Let's turn our attention now to stage.c, where the bulk of this first part resides. As with everything before, this will remain simple (and once again, we're skipping over things that have been covered in past tutorials - we'll refer back to them as needed).
Our first function is initStage:
We're clearing our `stage` object using memset, and then checking if we need to load our textures. groundTextures is an array of AtlasImages of a fixed length (NUM_GROUND_TEXTURES). When declaring our array, we're setting the first element as NULL, so we can test whether we have already loaded it. If we need to load the texture, we're calling loadTextures (we'll come to this in a bit). Next, we're calling initGround, to prepare the ground. We're setting Stage's `speed` to the value of INITIAL_GROUND_SPEED (defined as 2 in stage.c), and finally setting App's delegate's logic and draw functions points to doStage and drawStage, respectively.
Nothing we've not seen before. So, let's move onto initGround:
A pretty simple function. We're randomly setting the texture we want to use for our ground indices. `ground` is an array of ints, of GROUND_WIDTH in size (defined as SCREEN_WIDTH / GROUND_TILE_SIZE + 1). We're looping through each element in the array and setting a random number between 0 and NUM_GROUND_TEXTURES - 1 to its index. We'll be using this for our drawing. One can think of this is a 1D tile map.
With our ground tiles randomly redefined, we can move over to the first part of our game's logic step, in doStage:
doStage will ultimately be the main function of our game, that will be called from the game itself, as well as the title screen. Right now, this function does only one thing - calls doGround:
Once again, we're looking at a simple function. doGround decreases the value of groundX (a static variable in stage.c) upon each call, by the value of Stage's `speed`. This will ultimately cause our ground to move from right to left.
That's all our logic done! Now, let's turn to our rendering phase. Starting with drawStage:
drawStage will, in the end, handle all our game's rendering. Right now, however, it is delegating to another function - drawGround:
drawGround is where things get a bit more interesting, as this is where we'll be giving the impression of our ground moving.
The theory behind this is easy enough to understand - draw our ground tiles from left to right, in the position and order they are in according to the value of groundX. To achieve this, we're multiplying groundX by -1, to make it positive (remember, we're subtracting from groundX during our logic step), and then dividing the value by GROUND_TILE_SIZE to find out which tile index to begin at. We're assigning this value to a variable called `n`. We're then taking the GROUND_TILE_SIZE modulo of groundX, giving us a value between 0 and -(GROUND_TILE_SIZE - 1). This is our `x` position. Our drawing will therefore start from a position off the left-hand side of the screen or at 0, depending on groundX's value.
With our `x` offset known and our starting tile determined (`n`), we're setting up a loop to draw our tiles from left to right. Remember that GROUND_WIDTH covers the width of the screen, plus an extra tile for some overscan on the right-hand side, so our final tile doesn't "pop" into existence. We next setup a variable called `t` which will be the value of the index in our ground array that we want to use for rendering our tile. We add the current value of `i` (our loop variable) to `n`, and then take the GROUND_WIDTH modulo of this value, to give us an index between 0 and GROUND_WIDTH - 1, so that as we each the end of our ground array, we'll automatically wrap around again (assuming an array length of 25 - 22, 23, 24, 0, 1, 2, 3 ...). We then draw the ground tile using blitAtlasImage, indexing the wanted tile using the value at `ground`, indexed by `t`, positioned at `x` and GROUND_Y. Finally, we increase the value of `x` by the value of GROUND_TILE_SIZE.
It might look complicated, but it's really quite simple If you wish to see what is happening with the `n` and `x` variables, you could add printf statements to debug the values and see how they are working. You will see the value of `n` continue to increase from 0, and x go from 0 to -GROUND_TILE_SIZE - 1 (negative numbers).
There were a number of ways that we could have approached this. One alternate method would be to create a struct holding positions and texture of each section of the ground, and moving them from right to left, wrapping them back to the right as they moved off the screen. This would work, but is slightly tricker to accomplish correctly, due to the prospect of misaligning the objects when wrapping them to the right. Doing so incorrectly would result in ugly seams and gaps.
Just one function left to look at in stage.c, which is loadTextures:
Not a lot to see here! We're just looping through NUM_GROUND_TEXTURES and using a formatted string to load our groundTextures (named gfx/ground01.png, gfx/ground02.png, etc). These images are expected to exist in our atlas.
That's stage.c finished, so finally, we'll be calling initStage from our main function:
You'll notice this is the standard way we've been starting our games: calling initSDL, initGameSystem, and setting up a loop to process our inputs, and call our logic and draw delegates.
A gentle start to our game, but one that sets up some good foundations (one could even say 'ground work') to move forward with. If you've ever wanted to create an infinite scrolling ground layer, here is one way that it can be achieved. In our next part, we'll look into introducing houses, to go along with our ground.
The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle: