Saving and loading the game is a fundamental feature in many video games. This saving process is a form of serialization, converting game states to a byte sequence that can be stored/transmitted.

In the case of a chess game, it can be quite simple: the chessboard, a plain surface to be saved. In some first person shooters the positions of players and NPCs are stored, perhaps with some extra level progression information. But in a strategy/simulation game, the information that need to be stored can be more complex. Such as in Roller Coaster Tycoon, there are many peeps in the park, all having different behavior and short term goals. And these goals are connected with other objects in the game, such as a ride, or the restroom when they need to go somewhere. The laboratory system had a similar challenge because everything is connected.

ser_graph_t

This is a visualization of a save for a simple scenario with 3 ground structures, which are just towers. The icons mean the objects in the game. And the lines are the connections. The items in the 3 gray area are approximately the items that make the 3 towers.

ser_graphlocal

When parts stick together they will have connections to each other. Lets take a look at the smaller example shown above. A cube block has a port on each of its 6 faces. These ports connect to the ports on other parts. That is how parts build on other parts works. You can see many “BlockPort” surrounding the cube icons and connecting to each other. All together, the entire structure is considered a unit, which can follow commands the player gave based on its capabilities. Despite a ground tower can not move, one command is like move to a position, called a waypoint in the game system. As you can see the UnitObject and the WaypointNode on the left.

The software system rebuild these connections when loading a saved game. It does so by mapping old pointer addresess to new pointer addresses. And mostly due to the usage of objects from external libraries, the pointer resolution is a multistep process. I am not going to get into too much tech details about that. But it need to be reliable, because an unresolved pointer is invalid and will make the game crash.

 

In addition to the connections, each of these nodes has its own data. The number below the name means the size of the data in bytes. Think of parts have their health points, positions etc that need to be stored. Different types of items store different things, and can have vastly different sizes. A GridPlane consists of the grids on a buildable surface. It has information for each individual cell.

Here is another challenge. There are 100-200 different types of things and I need to write code to handle each one. And each one may have 10-100 different attributes(data member). Unfortunately, in c++ you do not have reflection information to do this automatically. So I wrote code to handle them all.  Here is the loading/saving code for a debris in the game:

void BlockDebris::deserialize(Deserializer* des, ObjectDataStorage* data)
{
	SO_READ(m_mass);
	SO_READ(m_lifeTime);
	SO_READ(m_level);
	SO_READ(m_position);
}
void BlockDebris::serialize(Serializer* ser, ObjectDataStorage* data)
{
	SO_WRITE(m_mass);
	SO_WRITE(m_lifeTime);
	SO_WRITE(m_level);
	SO_WRITE(m_position);
}

Just to give a simplified example. 2 operations, serialize and deserialize. 4 attributes in a BlockDebris. Occasionally I make mistakes. If I accidentally missed one attribute, some issues arise. This is what it looks like if I forgot to load the position. Things collapse to the origin point.

ser_bug

To make this easier, I wrote a meta data class to store meta data about these attributes, and select appropriate read/write routine at compile time using meta programming. How this class works is a different story. But the purpose of this is to merge the 2 repeated workflow to one. Then I will not accidentally miss one in one of operations. This meta data is stored by type, so no memory overhead on class instances.

const ClassDataFieldGroup BlockDebris::MetaData =
{
	MD_FIELD(BlockDebris, m_mass),
	MD_FIELD(BlockDebris, m_lifeTime),
	MD_FIELD(BlockDebris, m_level),
	MD_FIELD(BlockDebris, m_position)
};
usage: MetaData.ReadAll(this, data);

If things are not going well, I may take a step further by generating this list automatically using a tool. But that will need some extra work writing the tool and tagging data members in c++ headers.

This serialization challenge has been a backlog item for quite a while. Now it has been finally worked out. At this point there is no tricky features anymore, so gameplay development will be smoother next.