• 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
— An old-school isometric game —
Now that we can draw tiles and select them with a mouse, it's time to add in some entities. Rendering our entities isn't too different from rendering our map tiles, however, we need to ensure that our entities are drawn on top of our map! In order to do that, we're going to introduce layers, and assign our ISOObjects a layer upon creation. Those ISOObjects at the bottom will be drawn first, with those higher up being drawn next. We'll support 10 layer levels, moving from background to foreground, to allow for space in-between.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./isometric03 to run the code. You will see a window open like the one above. The glasses will be randomly placed with each run, but otherwise there is not much else to do. Once you're finished, close the window to exit.
Inspecting the code
Adding in our layering and entities is very easy, now that we've got the groundwork out of the way. To begin with, let's look at defs.h:
We've added an enum containing LAYER data. LAYER_BACKGROUND is the map layer, LAYER_MID is essentially a middle layer, while LAYER_FOREGROUND will be where our entities live. Notice how we're spacing them at 5 value increments, to allow us to place things in between, if required. We're also not limited to these increments, and could easily have used 50 and 100.
Moving on to structs.h now, we've updated ISOObject:
We've added in a new field here called `layer`. This will be the layer to which the ISOObject belongs, and will affect its rendering order.
Next, we've added in the Entity struct:
This struct will be used to represent an entity in our game, for example, the water glasses. `name` is the entity's name, `x` and `z` are its map coordiates (tile indexes), `texture` is its texture. `base` is a special field that will be used to align the entity when we render it. We want to vertically align our object where it stands. Since we're working with isometric objects here, we need to manually specify this value, as attempting to calculate this value will only take us so far, and will always appear a little off.
We've also updated World:
We've added in entityHead and entityTail, to act as a linked list for our entities.
Now, let's move onto glass.c, where we're defining our water glass. This file only has one function, initGlass:
A very basic function, it takes an Entity (`e`) as an argument. We're then setting the entity's `texture`, and setting its `base` to -1. This will be our vertical alignment adjustment. Note that we're using the EntityFactory pattern in this tutorial, as with our tutorials, so we won't touch on how that works.
Onward to entities.c, where we're handling our entities. Quite a few functions here, though none of them are very taxing. Starting with initEntities:
We start by setting up our linked list, with a memset and an assignment of the tail to the head. Next, we're calling addEntities, to randomly add some entities to the map (in this case, just some water glasses), and finally loading a texture to use as a shadow, and assigning it to a variable called shadowTexture.
Next up, we have drawEntities. This is the interesting function, where we're actually drawing our entities as isometric objects:
For each entity in the world, we're going to be creating an ISOObject. To begin with, we're assigning a variable called `sx` the value of half our TILE_WIDTH, minus half the entity's texture's width. This will essentially align the entity in the horizontal middle of the tile. Next, we're assigning `sy` the value of half TILE_HEIGHT, subtracting the entity's texture's height, and then further subtracting the value of the entity's `base`. What this does is align the bottom of the entity's base to the middle of the tile, and then make a manual adjustment via the `base` value. In the case of our glass, this adjustment is minor, but for larger objects, it will be a lot more. Note that the only reason we have this `base` value is because we're aligning our entities to the middle of their tiles. If we didn't want to do this, the `base` variable wouldn't be needed. In other words, it's simply meta data (another approach that could've been used would be to make all our entity textures the same size, and centering the images within those bounds).
Now that we have our `sx` and `sy` values, we call addISOObject. As with our map tiles, we're passing over the entity's `x` and `z` values, but are also now passing over the `sx` and `sy` values, to perform the position adjustments. The texture is passed over, as expected, but we're now also sending over a new value: LAYER_FOREGROUND. This is used to tell our ISOObject that is it in the foreground layer. We'll see how this is used a bit later on.
With our entity added, we then add in the shadow. We're calculating the `sx` and `sy` adjustment values as needed (using the shadowTexture's width and height), but passing over LAYER_MID when calling addISOObject. We want the shadow to appear under the glass, but also above the map. LAYER_MID has a value halfway between LAYER_BACKGROUND and LAYER_FOREGROUND, which is just what we want.
That's our entity drawing handled. We'll be expanding this a lot in future. For now, let's move onto the rest of entities.c. Next up is getEntityAt:
No real surprises here. The function takes to variables: `x` and `z`, and looks for an entity with `x` and `z` values that match those, and returns it.
Next up is addEntities:
This function merely randomly adds 12 glasses to random locations around the map.
The placeRandom function follows:
All this function does is pick a random spot in the current map view (we're not using the whole map right now, just a portion, as denoted by MAP_RENDER_SIZE), and assign the values to `x` and `z`. Note that `x` and `z` are pointers to variables. We do this by setting up a while-loop that repeats until we find a location on the map that is a ground tile, and also isn't already occupied by another entity.
That's entities.c finished with, so we can move onto iso.c, where we've made a few tweaks.
Starting with addISOObject:
We've already seen that we're passing over the layer to this function; we've added it in as the last parameter. We're then setting the ISOObject's `layer`'s value to that which we passed in.
The drawComparator function has seen a bit of a larger update:
We're now comparing the ISOObject's `layer`s in the very first instance. Those with a lower layer value will be moved towards the top of the list, so we draw from "bottom to top". If the result of this calculation is 0, meaning they both occupy the same layer, we then test the `y` values as before. However, now, if the result of that calculation is 0, we then compare the `sx` values of each (the actual screen coordinates), taking into account the texture widths. This test ensures that the wider of the two entities is drawn first. This helps clear up a situation where a large entity behind another ends up drawing over the top of one closer to the front (this is an example of where isometric sorting can become complicated, due to using sprites).
That's iso.c updated. We just need a few little tweaks to world.c, and this part is done.
Frist, to initWorld:
We're simply calling initEntities.
Next, we've updated `draw`:
Rather predictability, we're now calling drawEntities.
And that's it, we have entities rendering. We'll now be able to add in lots of other entities, and have them render on the map, just as we expect (more or less!). What we should do next is introduce Purple Guy, the main character, for the player to control.
So, in the next part, we'll be adding in an entity for the player to move around the map, by clicking on tiles of the target destination.
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: