— Simple 2D adventure game — Note: this tutorial assumes knowledge of C, as well as prior tutorials.
Introduction Even though this tutorial consists of one dungeon area, hardcoding the entities and all the rest isn't the best approach; it would be better to load it all from a file, so we can support multiple dungeons. In this tutorial, we'll see how this can be done. Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./adventure09 to run the code. You will see a window open like the one above, showing the prisoner on a tiled background. Use WASD to move around. Don't be surprised that it appears nothing has changed, as this dungeon layout and the entities within it are identical to the hardcoded version. Close the window to exit. Inspecting the code Before we look at the code itself, let's consider how we are handling the entities themselves. They are all stored in the file data/entities.json. This is, as the name implies, a JSON file. We're using JSON as it's easy to read and work with (note: the file has been crafted by hand, no tools were used). You can see the listing below: [ { "id" : 0, "type" : "player", "x" : 41, "y" : 5 }, { "id" : 1, "type" : "gold", "x" : 1, "y" : 19, "value" : 1 }, { "id" : 2, "type" : "gold", "x" : 27, "y" : 0, "value" : 1 }, { "id" : 3, "type" : "gold", "x" : 19, "y" : 9, "value" : 1 }, { "id" : 4, "type" : "gold", "x" : 7, "y" : 3, "value" : 5 }, { "id" : 5, "type" : "chest", "x" : 21, "y" : 14, "itemId" : 7 }, { "id" : 6, "type" : "chest", "x" : 18, "y" : 14, "itemId" : 8 }, { "id" : 7, "type" : "item", "name" : "Eyeball", "x" : 0, "y" : 0, "texture": "gfx/entities/eyeball.png" }, { "id" : 8, "type" : "item", "name" : "Red potion", "x" : 0, "y" : 0, "texture": "gfx/entities/redPotion.png" }, { "id" : 9, "type" : "item", "name" : "Rusty key", "x" : 51, "y" : 23, "texture": "gfx/entities/rustyKey.png" }, { "id" : 10, "type" : "signpost", "x" : 54, "y" : 0, "message" : "Dead end. Try going another way." }, { "id" : 11, "type" : "signpost", "x" : 55, "y" : 14, "message" : "Golf sale." }, { "id" : 12, "type" : "signpost", "x" : 14, "y" : 28, "message" : "Insert player start position here." }, { "id" : 13, "type" : "signpost", "x" : 20, "y" : 16, "message" : "Two chests, one key." } ] Here, we have a JSON array, with each entitiy being represented by a JSON object. Each entity has at least an id, type, name, and x and y coordinates. There are other values that are specific to the type of entity we're working with, but we'll see more on all this in a bit. Our approach to loading the entities will be as follows: identify the type of entity we want to load, and call an appropriate init function. To do this, we want to create a structure that will hold the name of the entity type, and a pointer to an init function. We've created such an struct in structs.h:
The InitFunc structure will basically act as a key-value pairing, with the value being a function pointer that accepts an Entity. It is also a linked list. While we're looking at structs.h, we should mention the changes we've made to Entity:
We've added in an id and a new function pointer called load, which accepts an Entity (which will be the entity itself) and a JSON object. These will come into play when we're loading the entity. We've created a new file called entityFactory.c, which will be used to produce our entities as we load them. It consists of just three functions, but does quite a lot with them. Let's go from the top, starting with initEntityFactory:
The function will create our map of entity initFuncs. InitFunc is a linked list, so we first prepare the head and tail of the list, then call addInitFunc for each of the entity types we want to handle. addInitFunc is a convenience for creating an instance of an InitFunc struct, as seen before:
Again, this function prepares an InitFunc by mallocing and memsetting it, before then adding it to the list of other initFuncs. The important parts are the parameters values. type is the entity type we're going to handle, while init is a function pointer. If you refer back to our entity JSON file, you will see that each entity object has a type field. This type field is what will be used to lookup the initFunc. The init function pointer itself will be used to create the entity in question. If you open up entityFactory.h, you can see several externs such as initPlayer, initItem, initChest, etc. Notice how each one takes nothing more than an Entity. This function signature is expected of our function pointer. If that seems a bit confusing, don't worry. It will all make sense as we look at the last (and most important) function: initEntity.
initEntity takes a cJSON object as a parameter to work with. This function will grab the type field from our JSON object (which would be something like "player", "item", "gold", etc), and then loops through our list of initFuncs, looking for a matching type. We do this simply by performing a strcmp against the initFunc's type and the type string from the JSON object. If a match is found, we'll move onto the entity generation phase. We'll call spawnEntity to create an entity, and then assign its id, x, and y values from those contained in the JSON object. These three are always expected to be found in the JSON object. We can optionally also copy across the name of the entity, if one has been specified. With those taken care of, we then call the initFunc's init function pointer, passing over the entity we've created. This call will delegate to whichever function pointer was linked to the initFunc, such as initPlayer, initGold, etc. We'll see some examples of these shortly. After this, we can optionally call the entity's load function, if it has been set. Remember how we added a load function pointer to our Entity struct, which took the entity and the JSON structure as parameters? This step will basically do any additional loading work that is not covered by this general function. Again, more on these later. The last thing we do is update the dungeon's entityId to the highest value we know (either the id of the entity or the dungeon.entityId itself), plus 1. The reason we do this is because we want all our entity ids to be unique. If we don't do this, we could end up with duplicates if dungeon.entityId increments from zero, but our loaded entity jumps ahead or behind. Choosing the max ensures we always get the highest value. Again, why this is important and why we did it will be seen later. With that done, we can now look at how entityFactory's functions are used. To start with, let's look at the changes made to entities.c. We've updated initEntities and added some new functions. Starting with initEntities:
initEntities now sets the dungeon's entityId to 0, and also calls loadEntities and postLoad. We'll come to each of these in turn, starting with loadEntities:
loadEntites is a basic function. It opens our data/entities.json file, parses the JSON, and then iterates through each object, passing it over to initEntity, for processing. Once this is finished, it frees all the data that was loaded. postLoad is somewhat more specialist in nature. The purpose of this function is to perform any necessary tasks once all our data is loaded, such as correctly assigning items to owners. Right now, postLoad is handling our Chests:
Looking at the JSON data at the top of the page, you'll see that Chests have an itemId member. This is the id of the entity that should live inside the chest, as its item. The postLoad function is basically looking for the items that belong in the chests. While each entity can have its own load function, to handle additional data, the reason we're doing this here is because the item the chest requires might not have been loaded before the chest itself, which would therefore make it impossible for the chest to assign it as its item. This is also the reason why every entity in the dungeon how has an id, so that it can be looked up for purposes such as this. So, we want to populate the chest with an item. We do this by simply calling a function called getEntityById and then calling removeEntityFromDungeon, to ensure it doesn't remain in the dungeon. The getEntityById function is simple:
And as can be seen, it does exactly as its name implies: it looks up an entity by id, by iterating through all the entities in the dungeon and returning the one with the matching id. It will return NULL if it doesn't find a match. We should now look at the changes made to the entities themselves. We won't cover all of them, just a handful to get the idea. Each of the entity type's main init signature has been changed. They now all take an Entity as a reference, instead of spawning one themselves. Taking Gold as the first example:
As we saw in the entityFactory.c, the Entity is spawned there and passed into the init function. The gold entity now creates a Gold object to use as its data, and assigns its touch and load. Nothing more. This is because the job of loading the texture has moved to the load function:
The load function, as we've already seen, takes the entity and the entity's JSON object as its arguments. In the case of the gold, we want to extract the "value" key from the JSON and assign it to the gold's value, to indicate the value of the gold. Next, we'll assign an appropriate texture. If there are more than 1 coin, we will use the gfx/entities/goldCoins.png texture, otherwise, we'll use gfx/entities/goldCoin.png. We've also made a modification to the touch function, to handle this:
Now, when the player touches the gold, the amount of gold they receive is based on the Gold's value variable. The information message will also change depending on how many gold coins were picked up, either saying we picked up a coin, the telling us the exact number. We'll briefly look at some of the other entities that have changed, starting with items. The initItem function contains very little, just assigning the touch and load functions:
Since items are very genertic, being little more than a name and a texture, this is enough. We load the texture in the load function, using the "texture" JSON key:
As we already explained, the Chest no longer accepts the item it contains as a parameter, again just accepting the Entity itself:
We set the Chest's texture, solid state, and its touch and load functions. The load function is also very simple:
This is where we grab the id of the item that will reside in the Chest, using the postLoad function. Finally, we should see where we call initEntityFactory. Because this is a function we only want to call once, we're doing it in initGameSystem:
If we did this in initDungeon, we could end up registering lots of initFuncs by mistake, wasting memory. Calling the function in initGameSystem (called by main.c) ensures that this only happens once. This all our prep work for our dungeon adventure game done. We can finally move onto the phase of starting to turn it into a game. In the next tutorial, we'll look at implementing the game proper, and working with a larger dungeon. Purchase The source code for all parts of this tutorial (including assets) is available for purchase: From itch.io It is also available as part of the SDL2 tutorial bundle: | |
Desktop site |