SaveGood
Saving is the "easier" part in the Cultura project. What's being used is the simplest thing that can be done for now which is serialization into a bytestream and then writing this to a file. Difficult to debug and difficult to maintain but extremely fast. Essentially, each class that needs to have data save implements the serialize function, deriving from a class I called GameEntity (for you Java folks just pretend it's like Object). From here you can do whatever you please.
For primitives I simply cast their address to a char* and then save bytes into a byte buffer equal to their sizeof(). For objects that are not pointers, I call their serialize function. For objects that are pointers I can do the same. For arrays and other objects with size, I serialize their size and then serialize each individual object. The resultant byte stream is then directly written into a .sav file on disk.
Key notes here are that dependencies are not serialized (for instance, someone with a reference to AppStateManager will not serialize the AppStateManager and cause an infinite loop of serialization). Serialization is strictly "downstream" in the class hierarchy. Second, the graphics components are not serialized. I call createScene after I'm done deserializing when I load in order to recreate all the graphic entities.
LoadGood
Loading is the trickier part of deserialization although all of the problems are of course because you didn't serialize properly :). A file is loaded up, the contents are read and then put into a bytestream. This buffer is used for deserialization.
For primitives I can simply read a number of bytes and then assign the value I get to a primitive. For instance, read sizeof(int) bytes and put it into an int. Whatever value you get is what it is. Yay!
Objects are somewhat trickier. The constructor for all my classes that are serializable have a default constructor to make it easier to do this. In the default constructor, non-pointer member objects are constructed with their respective default constructors. When deserialization occurs, those objects can simply call .serialize(ByteBuffer) and for pointer objects then we create a new instance (using the default constructor) and then call ->serialize(ByteBuffer).
For arrays and the like we have to get the size, which we saved, and then resize arrays or unordered_maps and such and then begin to deserialize objects or primitives and then add them. For certain containers, we don't serialize the container, we merely serialize the contents. For instance, we can serialize the list of units in a faction and then create a new QuadTree and then add them to the QuadTree (so upon loading it is a fresh QuadTree with the saved units).
Dependency injection is terribad. After creating new objects you must then inject dependencies again since you cannot save pointers in serialization (what would it point to?). This creates very strange and complicated deserialization call trees. The worst of it in Cultura were actions which require pointer to objects to allow it to manipulate the game world. Poop.
And here is the result of loading, the unit is still carrying out the action that was queued, a harvest node action.
Menu Upgrade
The menu automatically generates the file name. I'll leave it in automatic generation even to release to avoid problems with bad user input. Both the save/load menu refresh each time you go back, looking at the files that exist in the save directory. It is also capable of stripping the .sav from the file name to show the user a nicer looking name (although right now it's just numbered from 0 to whatever). There are no loading screens but in the work needed to perform loading, quitting the game, going back in, creating a new game, saving the game or loading the game all work as expected (game is cleared, there's no accidental double deletes, there may be a little bit of memory leakage but I haven't checked yet).
Now you can SaveGood and LoadGood
No comments:
Post a Comment