• 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
— A simple turn-based strategy game —
It's time to increase the size of the map, and, in turn, add in the camera. We're going to be adding in a smooth scrolling map, as found in SDL2 Gunner, rather than the "jump" scrolling we have in SDL2 Rogue and SDL2 Adventure.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./sdl2TBS13 to run the code. You will see a window open like the one above, showing three wizards in a large room, surrounded by walls, as well as a number of ghosts. Play the game as normal. To scroll the map, use the WASD control scheme. Notice how the map attempt to keep subjects on screen during movements; when the player select mages, either with the mouse wheel or a click; and, when attacks occur, the target is centered. Once you're finished, close the window to exit.
Inspecting the code
Adding in our camera and increasing the size of our map is a simple thing, indeed. It just means having to update a lot of files!
Let's start with defs.h:
We've increased the values of MAP_WIDTH and MAP_HEIGHT, and also added in MAP_RENDER_WIDTH and MAP_RENDER_HEIGHT, to deal with our map rendering as we scroll it around.
Moving next to structs.h, we've updated Stage:
It now contains a struct called `camera`, that will hold our camera data. We're planning on updating this struct in a future part.
Now, let's look at camera.c. This is a new compilation unit, that will hold all the code for our camera. We'll start with clipCamera:
Much like the updateCamera function in SDL2 Gunner, this function limits how far the camera is allowed to scroll, making use of the MAX and MIN macros, applied to both the camera's `x` and `y`. Note how we're not clipping to camera to 0,0 or the width and height of the map. We're allowing the minimum and maximum to be adjusted by W_PADDING and H_PADDING. This basically gives us some overscan, so we can scroll beyond the limits of the map. This can make things more comfortable for the player, as they can move the corners of the map closer to the centre of the screen.
Next up is centreCameraOn:
This function is used to centre the camera on an entity (`e`). We convert the entity's `x` and `y` values to screen coordinates, then subtract half the screen width and height, and assign the result to the camera's `x` and `y`. Finally, we call clipCamera, to ensure we don't stray outside our allowed map overscan.
The last function is ensureOnScreen:
As it name suggests, this function will ensure that an entity (`e`) is visible on screen. To achieve this, we want to make sure that our entity is contained within a certain area of the screen. We take the entity's `x` and `y` values, and subtract the camera's `x` and `y`, divided by MAP_TILE_SIZE. This results in `x` and `y` having values that can be evaluated as though they are indexes of the map. We then test the values of `x` and `y`. If `x` or `y` are less than ENT_ON_SCREEN_BOUNDS, we will call centerCameraOn, passing over `e`. Equally, if `x` or `y` are outside of MAP_RENDER_WIDTH less ENT_ON_SCREEN_BOUNDS or MAP_RENDER_HEIGHT less ENT_ON_SCREEN_BOUNDS, respectively, we'll also call centerCameraOn. Ultimately, this will mean that calling this function will keep the subject entity in the middle of the screen. We'll see this in a use in a little while.
That's camera.c done, for now. Next, let's head over to player.c, where we've made a number of little updates. Starting with doControls:
We're now testing for our WASD control screen, to move the camera around. Based on the keys used, we'll either increase or decrease the camera's `x` and `y`, by CAMERA_SPEED. This will cause the camera scroll smoothly around.
Next, we've updated cyclePlayerUnits:
After completing our while-loop, we've added in a call to ensureOnScreen, to make sure that the unit we've newly changed to is on screen. This means that as we scroll through our available mages, the camera will jump to them, rather than us having to find out where they are (but only if they aren't already visible on screen).
We've done the same thing with doSelectUnit:
Now, after selecting a mage (or cycling through the unit's actions), we're calling ensureOnScreen, in case the mage is at the limits of the screen. Again, we're doing this to make things a little more comfortable for the player.
Now, let's have a quick look at how the addition of the camera is affecting our rendering of entities and the map. Once again, we've actually covered this in past tutorials, so we won't linger or go into depth here.
Looking first at units.c, we've updated `draw`:
We're now subtracting the camera's `x` and `y` from the screen coordinates of the unit, before drawing. This will ensure that the unit is drawn in the correct place.
As another example, we've updated damageText.c:
Yet again, we're subtracting the camera's `x` and `y` from the damageText's `x` and `y`, to make sure we display it in the correct location on screen.
We've done the same for all entities and effects, int their respective functions, but we won't detail them all.
Briefly, let's look at map.c and the drawMap function:
We've updated this function to now take into account the position of the camera. This rendering technique was covered in SDL2 Gunner, so we'll say nothing more on it here, only that we've implemented it. Note that our tile drawing, with the ranges, remains the same.
Unlike SDL2 Gunner, however, we're not using a quadtree in this game, as there is no need. To that end, we want to ensure that we only draw the entities that are actually on screen right now (something the quadtre was helping us with in SDL2 Gunner). Let's take a look at the updates to entities.c, to see how this is done.
Start with drawEntities:
Before, we were simply rendering all the entities. Since our map was small and was contained within the one screen, we didn't need to worry about drawing entities that were outside of our view. Now, we're going to take this into account. The idea is to test all entities in the game, to see if they are visible, and add them to a draw list, beforing rendering them.
We start by setting a variable called `n` to 0. This will be a count of all the entities that are on screen at this time. Next, we loop through all our entities, and pass them into a function called isOnScreen. We also test that `n` is less than MAX_DRAW_ENTS, to make sure we don't overwrite our array. Should this test pass, we add the entity to drawList.
Next, we're using qsort to sort the list of entities to draw. This step will help to draw our entities in the correct order. You may have noticed in previous parts that when a mage or ghost stops over an item that the item is drawn on top of them. This is because we're rendering the entities in the order of the list, which isn't desirable. Since items are the last things to be added to our entity list, they are drawn last and obscure our entities.
Finally, with drawList populated, we loop through all the elements and draw the entity at the index.
The isOnScreen function follows:
This is quite a lot like the ensureOnScreen function in camera.c. However, ror the purposes of rendering, we're allowing our entities to be slightly outside the bounds of the screen, so they don't suddenly pop into existence as we scroll.
Finally, the drawComparator:
This is the function we're feeding into qsort. We'll sort our entities by their type, with those with a lower type value being pushed to the bottom. According to our ET enum order, ET_ITEM will be drawn first, with the other things drawn afterwards. This means that items cannot obscure our units.
We're almost done! We're going to take a quite look at where else the ensureOnScreen function is used, and then we'll be wrapping this part up.
Turning first to bullets.c and the fireBullet function:
At the end of the function, we're calling ensureOnScreen. Note that we're passing over Stage's targetEntity. The reason for this is because we want the target of the attack to be visible, rather than the attacker. It's no good if the attacker can be seen clearly, but victim is invisible due to being offscreen. If the player is being attacked, they'll want to know which of their units is the target.
We've added ensureOnScreen to the move function in units.c:
As our unit moves, we're calling ensureOnScreen, to keep them on screen. This prevents the unit from walking off screen and their movements becoming lost to the player.
We've also added ensureOnScreen to resetUnits:
Doing this makes sure that when the player's or AI's turn begins that the camera focuses on the unit that has now become activated.
Finally, let's head to stage.c, and update the `logic` function:
After almost everything is done, we're calling clipCamera. We're doing this here to centralize the logic, so that we don't need to keep calling it every time the camera is moved about. Doing this near the end of the function means we're close to the rendering phase, so everything will draw as expected.
And that's our camera added in. This is infact not the end of our camera work, and in a future part we're going to improve it. For now, we have something that is functional and easy to work with.
What would be nice now is if we had a better HUD. We should be able to display messages and supply some basic UI elements to control the game. In the next part, we're going to look into adding these in.
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: