• 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
— Simple 2D map editor —
It's now time to start adding entities to our map. Our game supports a number of different items: lollipops and keys to collect, spike traps to avoid, and doors to open. There are also two flags: red and green, that mark the beginning and end of our stage, respectively. Like with selecting tiles, we'll be able to scroll through the available entities with the mouse wheel, and place them on the map. There are also some restrictions - only one player, red flag, and green flag is allowed, and they cannot be removed. We'll look at how this is handled as we go along.
Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./editor03 -edit example to run the editor. You will see a window open, displaying a scene like the one above. The controls from the previous tutorial apply. In addition, pressing 1 on the keyboard will enter tile edit mode, while pressing 2 will enter entity edit mode. As with editing tiles, the mouse wheel can now be used to cycle through the available entities. The currently selected entity is shown by the cursor, and also at the bottom of the screen. Click the left mouse button to place an entity on screen. Click the right mouse button, while over an existing entity, to remove on. Notice how the player, red flag, and green flag cannot be removed. In addition, attempting to add a new one will move the existing one to the new position. Once you're finished editing, press Space to save the map. If there are errors, these will be logged to the terminal. With your map saved, close the window to exit. As before, you can play the map by using ./editor03 -map example. The goal of the game is simply to collect all the lollipops on the stage, and make it to the green flag. If the player is killed by spikes, they will respawn by the red flag. Touching the green flag when all the lollipops are collected will end the game, and the program will exit.
Inspecting the code
This update touches on almost all of the functions in editor.c, so this part is going to end up being quite long. However, it will be simple to understand, as we'll see. We'll also snip out code we're not interested in, to reduce the noise a bit.
Firstly, we've added a new enum:
This enum set is prefixed with MODE_, and will be used to determine the mode we're currently operating in, whether that be editing tiles (MODE_TILES) or editing entities (MODE_ENTITIES).
Now onto initEditor:
The first new thing we're doing here is setting a variable called `mode` to MODE_TILES. The `mode` variable is going to be used throughout editor.c to determine what we're editing, and what sort of behaviour should be invoked. We'll see this in action in many functions to come. Next, we're calling a new function here named getEditorEntities. This function lives in entityFactory.c, which we'll come to at the end. Basically, we're grabbing an array of all the defined entities in our game, and assigning them to an Entity array (`entities`), much like our tiles. This will allow us to scroll through them in a similar way. totalEntities is an int, that will hold the total number of available defined entities. Next, we're assigning a variable called currentEntity to the first entity in our `entities` array. This variable will be used throughout the editor as the entity we wish to place on screen (again, much like the current tile).
We've next updated addDefaultEntities:
As previously stated, as with the player, the red flag (startFlag) and green flag (endFlag) are required, and so we create them here when starting a new map. They are placed on screen alongisde the player.
Now for the `logic` function:
A small update. We're testing to see if currentEntity is not null, and then updating it's `x` and `y` values. Much like with the tiles, we're snapping the coordinates to the grid. The grid while editing entities is much smaller, ENTITY_SPACING being defined as 8. This allows us a finer degree of placement when compared to the tiles.
Now we come to doMouse. This is the first of many functions that have been updated to handle the new `mode` variable:
Okay, so once again this is a large function. However, it's not as complex as it first appears, and it's rather simple to explain. Before this part, we were treating all the mouse clicks as though we were only editing tiles. Now, we're checking the value of `mode`, to see what we're working with - MODE_TILES or MODE_ENTITIES. The logic that used to exist to handle the tiles now lives within an if-block that executes if `mode` is MODE_TILES. If `mode` is MODE_ENTITIES, we're going to handle the entity logic.
First, we check if the left mouse button is pressed. If so, we're going to clear it, and then call addEntity (we'll see this in a moment). Compared to when placing tiles, we want to make sure we're pressing the button, so that we don't create 100s of entities all at once. Next, we test if the right mouse button has been pressed. Again, we clear the button, and then call removeEntity (again, we'll come to this). We then test the mouse wheel. If the wheel is moved down, we'll clear the input, and then call cycleEntity, passing over currentEntityIndex and -1. You'll notice this function's signature is very similar to cycleTile. This is because they both work in a similar way, as we'll see. We then set currentEntity to the entity at index currentEntityIndex. Finally, we check if the wheel has been moved up, reset it, call cycleEntity, this time passing over 1 to move forward through the array, and assign currentEntity as with the other motion.
As you can see, things are quite easy to understand - as with tiles, we're placing an entity when clicking the left mouse button, removing one with a right click, and scrolling through the entity list using the wheel.
Now let's look at the new functions we introduced to handle the entity editing. Starting with addEntity:
At first, one might think that we need only add an entity at the position where the mouse cursor is. However, since we have unique items, we need to ensure we don't create duplicates. Therefore, the first thing we do is test the editorFlags of currentEntity, to see if the EMF_UNIQUE flag (defined in defs.h) is set. If so, we're going to call a function named findExisting, passing over currentEntity's typeName, and assign the result to `e`, the entity we'll be working with. We'll see how findExisting works in a bit. If this isn't a unique entity, we'll just call initEntity, passing over the typeName of currentEntity, and assigning to result to `e`. This allows us to create lots of entities of the same type, such as lollipops or spikes. With all that done, we assign `e`'s `x` and `y` values to those of currentEntity's `x` and `y`. Remember that currentEntity is updated in `logic`, by tracking the mouse's position.
Pretty straightforward. Now for findExisting:
This function takes a single argument - the type name of the entity we're searching for (typeName). As expected, this function loops through all the entities in the stage, looking for one with a typeName that matches the typeName passed into the function. If an entity is found, we'll return it. Otherwise, we'll return currentEntity. So, when used with addEntity, this function will help to ensure that only one of the unique entities exists, with a new one being created if one isn't already present.
removeEntity is next:
This is a standard function to remove an item from a linked list, ensuring that things like the tail pointer are correctly handled. What we do is loop through all the entities in the stage, looking for one that overlaps our mouse pointer (camera position included) using the `collision` function, and also one that isn't flagged with EMF_REQUIRED, and delete it from the stage. We're testing for EMF_REQUIRED because we're not permitting the removal of things such as the player, the starting flag, or end flag.
That's our mouse logic done. We can now look at the updates to keyboard. Over to doKeyboard:
We've not added a lot in here! We're now testing if 1 and 2 on the keyboard have been pressed. If so, we'll clear the inputs, and update `mode` as needed. If we press 1, we're setting `mode` to MODE_TILES, to edit tiles. If 2 is pressed, we're setting `mode` to MODE_ENTITIES and assigning currentEntity to the entity at the array index of currentEntityIndex. Simples.
Another new function new - cycleEntity:
As previously stated, this function works a lot like cycleTiles. It takes a pointer reference to a variable (`i`), as well as a direction (`dir`). We then update `i` by the value of `dir`, and loop around if the value is less than 0, or greater or equal to totalEntities. Note that we don't need to use a do-loop here, as there are no gaps in our array; an entity will always be available at the array index we move to.
Our logic updates are finished! Now to move over to the rendering. Starting with `draw`:
A few tweaks here, but nothing difficult to follow. First, we're testing if `mode` is MODE_ENTITIES. If so, we're going to call drawGridLines, passing over ENTITY_SPACING. Compared to editing tiles, we're going to draw our gridlines first, to make our entities easier to see. The previous tile rendering logic has been shifted into an if-block executed if `mode` is MODE_TILES, and a new if-condition has been added to test if currentEntity is not NULL (this is to future-proof us, as we'll see in the next part). When currentEntity is not NULL, we'll be calling its draw function, to render it. Again, since we're always updating currentEntity with the mouse's position, it will follow the cursor. Next, we're calling drawOutlineRect, using currentEntity's `x` and `y` values as the coordinates, as well as its width and height (`w` and `h`), to place a yellow outline rectangle around it. Not much different from the tile editing mode..!
drawTopBar has also been updated, as one might expect:
And also as one might expect, we've updated the logic to test `mode`. The logic for rendering when in MODE_TILES now lives in the appropriate if-condition. When handling entities, we're once again testing if currentEntity is not NULL, and using its `x` and `y` values as the "Pos" text. Similiarly, we're drawing the typeName of the entity instead of the tile type.
Now for drawBottomBar:
Again, this function has been updated to include an if-condition check for `mode`, and will render the available entities along the bottom of the screen, much like with tiles. We're checking if `mode` is MODE_ENTITIES before doing so (we're don't want currentEntity to be NULL in this case). Much like when we were rendering our selection of tiles, we're assigning variables `i` and `j` to currentEntityIndex, moving `j` to the previous entity in the list, and then entering a while-loop to draw entities to the width of the screen. Notice here that we're using a gap of MAP_TILE_SIZE between each entity. None of the entities in our game are wider than MAP_TILE_SIZE, so we can get away with it. If we wanted something more accurate, we could use the entity's `w` value (its width). We'd also want to tweak the subsequent drawOutlineRect that follows, to take this into consideration.
That's all there is to editor.c. Before we wrap up, let's take a look at the editor flags (EMF) we've mentioned, to see how these are setup.
Turning first to player.c, we can see how are flags are applied in initPlayer:
As we can see, we're assigning the entity's (`e`'s) editorFlags as EMF_UNIQUE and EMF_REQUIRED. For the player, this means that it is a unique object, and also cannot be removed from the map; right clicking on it will do nothing, and attempting to add a new one will only move the existing one.
The same is true of the flags, as seen in flags.c, in the initFlag function:
Again, the editorFlags are set as EMF_UNIQUE and EMF_REQUIRED.
Lastly, let's turn to entityFactory.c, where the getEditorEntities function lives:
This function is responsible for generating our array of entities, for use with editing. It takes a variable pointer as a parameter (`total`), and returns an array of entities. The first thing this function does is loop through all the InitFuncs registered with the entity factory, to find out how many available entities there are. A variable called `num` is used to hold the value. With the number known, we malloc an array of Entity pointers, of length `num`, and assign it to a variable called `ents`. We then reset `num` to 0, as we'll be using this an index variable in the next step.
We once again loop through all our initFuncs, this time mallocing an Entity, and passing it to the current InitFunc's `init` function. We then set the entity's typeName as the initFunc's `name`, and assign it to the entity array at index `num`. So, in short, we're filling our entity array with an instance of a created entity. This will happen for every entity in the game.
Lastly, we're setting `total` to the value of `num`, and returning our `ents` array. The calling function will therefore have an array of all defined entities, and also know how long the array is.
That's our entity editing done. As you can see, there wasn't actually too much too it, and it was quite similar to handling our tiles. However, we needed to add in a lot of logic to check whether we were dealing with entities or tiles, and respond accordingly.
Now that we can add in tiles and entities it's about time we expanded the size of our map from this small space it currently occupies. In the next part, we'll look at doing that, as well as allowing entities to be picked and moved around.
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: