Technical details of GAE saved games.
This post is intended for others who may want to understand the technical details of how GAE is performing serialization and deserialization of game data. I wanted this available to others and I figured this is the best place as any.
First off, GAE is using the loosest coupling possible, preventing objects from knowing the details of each other. GAE is using the Shared::Xml classes to perform serialization, keeping a standard interface for reading and writing data to the file system. I have enhanced the XmlNode class to support a number of shortcut functions that reduce the amount of code I have to write to do common functions, like read or write a node that contains a single attribute named "value". Overall, using xml seems to be the cleanest approach for serializing game data.
XML has a lot of drawbacks, it's slower, clunkier and makes big files, but I'm depending upon more recent hardware to offset those drawbacks. Also, if we decide that we want saved game files to be smaller (I anticipate they will be 100k-ish), we can pull in the zlib library and compress them in gzip format. This leaves it possible for players to still manually uncompress them to hack them to cheat (and we all know we like to do that sometimes!!!
) upon which it writes the XmlNode object to the saved game file.
Game::save(string name) const
|
+-> GameSettings::save(const XmlNode *) const
|
+-> World::save(const XmlNode *) const
|
+-> Stats::save(const XmlNode *) const
|
+-> Faction::save(XmlNode *) const //for each faction.
|
+-> UpgradeManager::save(const XmlNode *) const
|
+-> data members resources & stores persisted with tight coupling (1)
|
+-> Unit::save(const XmlNode *) const //for each unit.
|
+-> Effects::save(const XmlNode *) const (2)
| |
| +-> Effect::save(const XmlNode *) const (3)
|
+-> Command::save(const XmlNode *) const //for each command
Notes:
1. This is one of the few exceptions to the loose coupling because it seemed to make more sense to do it this way.
2. This applies to data members Unit::effects and Unit::effectsCreated.
3. Bug: Effect isn't saving it's root ("root cause") data member, which only applies to recourse effects. Some effects have a recourse, an additional effect that is applied to the unit caused the original effect on it's target. An example in FPM is the Lich's soul steal effect. The effect "soul_steal" does damage over time on it's target. When the Lich uses this attack, he gets an effect named "soul_steal_recourse" applied to him that regenerates his health. These effects are tied together so that if his target dies, his regeneration stops. But if the Lich dies early, the "soul_steal" effect on his target is also supposed to end early. With this bug, that wont happen after reloading a saved game. In order to fix this, we would have to create some type of EffectReference class and have an "effect registry" in the World object so we can keep track of all effects in that way and look them up later. I'm not sure that's warranted.
Also of note, I learned a few things about why Martiño was using these UnitReference objects. For one, they are really easy to persist. Since, I converted my code to use a UnitRefernce object for all the pets collection object and the master. The data member (a.k.a. "field" is what I'm used to calling them since doing so much Java now) master is NULL if the unit is free (not a "pet"), otherwise, it points to the owner of the unit where pets is a list of units that are "owned" by the this unit.
Reconstituting the object stream is more tricky. I prefer to use a constructor that accepts a const XmlNode * object when possible, but some of the objects have to be constructed and initialized before deserialization, so this is what it ends up looking like. The code that initiates this chain of events is in MenuStateLoadGame::mouseClick().
XmlNode *root = XmlIo::getInstance().load("savegames/" + selectedItem + ".sav");
program->setState(new Game(program, root));
The root XmlNode object is later deleted by the Game object after it's done with it. Here is the sequence diagram:
Program::setState(ProgramState *programState) //in this case, the programState calls the Game constructor below.
|
+-> Game::Game(Program *program, const XmlNode *node)
| |
| +-> GameSettings::GameSettings(const XmlNode *node)
| |
| +-> World::World()
| |
| <--+ //control returned
|
+-> Game::load() (1)
| |
| <--+
|
+-> Game::init()
| |
| +-> calls some other objects we don't care about
| |
| +-> World::init(Game *, const XmlNode *)
| |
| +-> World::loadSaved(GameSettings *, const XmlNode *)
| | |
| | +-> Stats::init()
| | |
| | +-> Stats::load(const XmlNode *)
| | |
| | +-> Faction::load(const XmlNode *, World *, const FactionType *, ControlType, TechTree *)
| | |
| | +-> UpgradeManager::load(const XmlNode *, const FactionType *)
| | |
| | +-> //resources and stores deserialized using tight coupling
| | |
| | +-> Unit::Unit(const XmlNode *, World *, Faction *, Map *, const TechTree *) //for each unit
| | |
| | +-> Effects::Effects(const XmlNode *, World *, const TechTree *)
| | | |
| | | +-> Effect::Effect(const XmlNode *, World *, const TechTree *) //for each effect
| | |
| | +-> Command::Command(const XmlNode *, World *, const UnitType *, const FactionType *) //for each command
| | |
| | +-> reinitializes stats based upon upgrades, effects, levels, etc.
| | |
| | <-----------+
| |
| +-> Faction::reinit() //for each faction
| |
| <-------+
|
+- //game starts...
Notes
1. Loads tech tree, tileset, etc.
EDIT: Added call to Faction::reinit() that I forgot earlier. Also of note, I'm leaving out a lot of initialization details that aren't directly related to loading a saved game.