The title is a bit of a pun, because this week was spent primarily on the ‘story’ part of the engine. Last week I mentioned that the next step was to implement level and then move on to the skeleton of the room object. Before getting into Story, I’ll mention right now that I implemented level.cpp (in terms of functions within level and above, ie. calls to the room object are commented out), and I also made a lot of progress on Story, which is divided into story.h and story.cpp. I also needed to sort out a few git related issues, but they went smoother this time, so I think I’m getting more used to it.
A game is made up of many parts, some static and some changing. Traditionally, there are many elements of a game that are static, such as the level data (this is not to say that animated tiles are static, but the data itself drawn, compressed and stored separate from the game). The Immortal however, is no traditional game. The further I unravel this ball of knotted up cables, the more surprises I am met with. Sometimes that has me looking like this:
But other times, there’s something really interesting in there. For instance, allow me to tell you the story, of Story.GS.
This file serves the purpose of what is usually ROM data. Data which is created and stored before the game is compiled and run. It contains all the information to describe each part of a level. The game is made up of 8 levels, each of which contains numerous traps, enemies, objects, and environmental elements, spread across a selection of rooms, connected to each other by doors. A level has many properties that must be read from during gameplay and when loading the level. The position of every element within the game universe (the viewport essentially), where the player gets loaded in, the location of the entrance and exit from the level, and much more. All of this can be stored as simple data that gets read from by indexing the level and the room the player is in. Sounds pretty straightforward at first, but things get interesting very quickly. The first thing you would notice, is that almost nothing in the file is anything other than a macro. Every line is a macro, which again sounds straightforward. However looking into the macros reveals the true complexity of this subsystem. And when you read through all of the macros and start to connect it all together, you see that while yes, some of the file is standard looking static data, the majority is actually determined dynamically. We’ll get to that dynamic system in a second, but let’s start by just mapping the file out.
The blue represents the dynamic component we will see in a moment, while the light green represents data specific to the individual level, but not stored dynamically and is treated the same as the darker green section, which represents the main static globally accessible definitions.
In this context, a definition is a macro that writes a set of ROM data, which will likely be used by the main level data, but might also be used by any other part of the game engine. Ex. The title screen text sequence is a set of str definitions (not to be confused with string definitions, or the macro for strings. str is a string macro that is also used for not strings).
The global definitions are very straightforward, just a contiguous series of data like you would expect. It’s the level specific data that gets weird.
By weird, I mean that this set of ROM data (referred to inside the source code as ROM data in fact), is written in what I can only describe as a ‘compiler function’. It is not an explicit function of the compiler, nor is it a routine run in the game. It is a function in the abstract, that runs at compile time. The result of which, is a series of ROM data entries, which are able to reference each other, change what position it is relative to, and return information at the end. It is, quite interesting. There are variables like roomIndex and doorIndex, which get updated as the compiler processes the macros, which are then subsequently used by other macros. They are then cleared and used again, just like memory inside a running program. The most interesting of these however, must be the variable roomNum. This is a variable which is set dynamically based on what the compiler ends up with after processing the story entries. It is then used and affected outside of this compiler routine, in the actual game code. This brings up the question of ‘lexical’ vs ‘dynamic’ variables, but I won’t go into that in this post. Suffice it to say, by using this compiler function at compile time, the compiler is sort of reaching into the future running code with certain variables. It’s fascinating stuff, but ultimately it makes translation very confusing.
What I decided to do, was to translate this compiler function as a real function run during the initialization of level. This does change the individual level initialization, but I believe I have translated that part fairly accurately as well. To understand the structure I decided on however, we need to know the connections between the different dynamically declared data types:
This is not the best diagram, as it does not show every data type, nor does it show the connections in more than a cursory way, but I think it gets the point across. Looking at a single entry in Story, you are effectively looking at a tree of indirect pointers going deeper and deeper. Let’s take a look at a single object to understand better. We’ll use my C++ code instead of the source for clarity:
This is luckily not the most complex object, so we can map it out without too much trouble. But I think it conveys how potentially complex it can get. For example those two ‘kDoNothing’ parameters could also be Use or Pickup structs, leading to further functions from there.
So what I decided on, was to have a struct for each level entry, called Story. This struct has a list of rooms, flames (the torches), objects (items, chests, etc.), doors, and monsters. The universe properties normally in univAt are instead just properties of Story. The rooms, flames, objects, doors, and monsters are all structs as well, and in the case of Obj, they can contain more structs from there. Then, during initialization, all of these structs are populated completely. When a level is loaded, it does not have to search through all the story entries like the original, because they are now in data structures. Instead, it indexes to the story struct, and loops over all the rooms/doors/etc. in the struct and creates the relevant objects and data within the ‘room’ object. I believe this is a fairly direct translation of what is in my opinion, a very non-standard method of handling these data types in the original game.
For this next week, I will be continuing the work on Story I have been doing, as well as creating the skeleton of room, and hopefully I will fill out more if not all, of the story entries (filling out a story level is extremely tedious, it takes quite a while).
Thanks for reading, I’ll see you next week!