Before we go on, let me make it perfectly clear I am using the term ‘object’ in a philosophical sense here. It has nothing to do with object-oriented programming, nor does it have anything in common with the ‘Object’ type pre-defined in programming languages like Java, C# and Python. Below, I will define a new data type named object; any other name will do equally well if you find object to be confusing, or if it gives a namespace conflict in the programming environment you are using. Very well, moving on...
Most puzzles in adventure games revolve around items and/or persons. Examples:
Naturally, the guard might as well be a dog, troll, dragon or robot. In this sense, non-player character is a common term, but I don't want to make a distinction between player and non-player characters (the guard might well be another player in a multi-player game). And just ‘character’ is easily confused with a character data type, so I will stick to ‘person’.
To represent items and persons, we can use a struct like this:
struct object { const char *description; const char *tag; struct location *location; } objs[] = { {"a silver coin", "silver", &locs[0]}, {"a gold coin" , "gold" , &locs[1]}, {"a burly guard", "guard" , &locs[0]} }; |
Notice this data structure is very similar to the array of locations we made in the previous chapter. In fact, the two are so similar we can merge them into a single big list containing locations, items and persons, and simply refer to all of them as objects.
struct object { const char *description; const char *tag; struct object *location; } objs[] = { {"an open field", "field" , NULL}, {"a little cave", "cave" , NULL}, {"a silver coin", "silver", &objs[0]}, {"a gold coin" , "gold" , &objs[1]}, {"a burly guard", "guard" , &objs[0]} }; |
Now that there is no separation between objects and locations, the struct object contains a pointer to itself. This is nothing exceptional in C: a linked list works in a similar way, so don't be alarmed.
To make it easier to reference individual objects, we will define symbolic names for pointers to each element in the array.
#define field (objs + 0) #define cave (objs + 1) #define silver (objs + 2) #define gold (objs + 3) #define guard (objs + 4) |
Here are a few examples of how to use these pointers. The first one is an adaption of a code sample from the previous chapter, displaying the text “You are in an open field.”
printf("You are in %s.\n", field->description); |
The following piece of code will list all items and persons present in the cave.
struct object *obj; for (obj = objs; obj < objs + 5; obj++) { if (obj->location == cave) { printf("%s\n", obj->description); } } |
So what is the benefit of having a single big list of objects? Our code becomes simpler, as many functions (like to one above) only need to scan through a single list of objects, rather than three lists. One might argue that this is irrelevant, since each command applies to one type of object only:
But this separation is hardly realistic, for three reasons:
It is tempting to add some enum attribute named ‘type’ to struct object to help us distinguish between the different types of objects. However, objects typically have other characteristics that work equally well:
There is one more object we will add to the array: the player himself. In the next chapter, we will see the real benefits of this choice. For now, the only difference is in the way the player's current location is stored. In the previous chapter, there was a separate variable locationOfPlayer. We will drop it, and use the location attribute of the player object instead. For example, this statement will move the player into the cave:
player->location = cave; |
And this expression returns the description of the player's current location:
player->location->description |
Time to put it all together. We start with a whole new module for the array of objects.
Sample output |
Welcome to Little Cave Adventure. You are in an open field. You see: a silver coin a burly guard --> go cave OK. You are in a little cave. You see: a gold coin --> go field OK. You are in an open field. You see: a silver coin a burly guard --> go field You are already there. --> look around You are in an open field. You see: a silver coin a burly guard --> quit Bye! |
object.h | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef struct object { const char *description; const char *tag; struct object *location; } OBJECT; extern OBJECT objs[]; #define field (objs + 0) #define cave (objs + 1) #define silver (objs + 2) #define gold (objs + 3) #define guard (objs + 4) #define player (objs + 5) #define endOfObjs (objs + 6) |
object.c | |
1 2 3 4 5 6 7 8 9 10 11 | #include <stdio.h> #include "object.h" OBJECT objs[] = { {"an open field", "field" , NULL }, {"a little cave", "cave" , NULL }, {"a silver coin", "silver" , field }, {"a gold coin" , "gold" , cave }, {"a burly guard", "guard" , field }, {"yourself" , "yourself", field } }; |
Note: to compile this module, the compiler must support constant folding. This rules out some of the simpler compilers like Z88DK. Which is a shame, since that particular compiler might otherwise be used to port our 1980s-style game to a 1980s-style computer.
Next, we define some helper functions to facilitate the other modules.
misc.h | |
1 2 | extern OBJECT *parseObject(const char *noun); extern int listObjectsAtLocation(OBJECT *location); |
misc.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" OBJECT *parseObject(const char *noun) { OBJECT *obj, *found = NULL; for (obj = objs; obj < endOfObjs; obj++) { if (noun != NULL && strcmp(noun, obj->tag) == 0) { found = obj; } } return found; } int listObjectsAtLocation(OBJECT *location) { int count = 0; OBJECT *obj; for (obj = objs; obj < endOfObjs; obj++) { if (obj != player && obj->location == location) { if (count++ == 0) { printf("You see:\n"); } printf("%s\n", obj->description); } } return count; } |
Explanation:
In location.c, the implementation of commands look around and go is adjusted to the new data structure. The old array of locations is removed, and so is the variable locationOfPlayer.
location.h | |
1 2 | extern void executeLook(const char *noun); extern void executeGo(const char *noun); |
location.c | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <stdio.h> #include <string.h> #include "object.h" #include "misc.h" void executeLook(const char *noun) { if (noun != NULL && strcmp(noun, "around") == 0) { printf("You are in %s.\n", player->location->description); listObjectsAtLocation(player->location); } else { printf("I don't understand what you want to see.\n"); } } void executeGo(const char *noun) { OBJECT *obj = parseObject(noun); if (obj == NULL) { printf("I don't understand where you want to go.\n"); } else if (obj == player->location) { printf("You are already there.\n"); } else { printf("OK.\n"); player->location = obj; executeLook("around"); } } |
Explanation:
The module main.c remains unchanged, you can see it in the previous chapter.
Again, feel free to experiment by adding more objects to the array in object.c. Do not forget to increase endOfObjs in object.h accordingly, or the additional objects will be ignored.
Compared to the previous chapter, our freedom of movement has only gotten worse. It has now become possible to ‘go’ to every object you like - even yourself! This will be fixed in chapter 6. But first, we will add some more commands.
Next chapter: 5. Inventory