Saturday, May 31, 2014

GuildCraft Dev Diary: Part IV

Serialization is a bitch.

It is also necessary. Serialization allows players to save their games and testing to be automated. C# offers three methods of serialization: XML, Binary, and Data Contracts. This article will focus on binary serialization and static instances.
( http://msdn.microsoft.com/en-us/library/ms233843.aspx )
( http://msdn.microsoft.com/en-us/library/ms733127(v=vs.110).aspx )

Data contracts are used to translate information between different type implementations. A common application is client-server systems. As long as both satisfy the data contract, information can be exchanged. Since I want my data to restore as the same type it was saved as, writing data contracts is overkill.

XML serialization was my first choice when I approached this feature. The main advantage it has is that the files are human-readable. It is trivial to open a save file and modify a numeric statistic. It is also possible to spot serialization errors without deserializing the save file. The default XML serialization in C# is actually quite nice. It is easy to write a helper method that takes a reference to a type and spits out an XML file.

Which is worse, cheating with a strategy guide or notepad.exe?

There is a major catch to using XML. Only public properties and variables are saved. This conflicted with a previous design decision I made to restrict most properties and fields to readonly or private. This is sometimes referred to as encapsulation. Custom extensions to standard XML serialization are possible, but rely on reading/writing elements in the correct order. This is extremely prone to mistakes and it is a big headache to write serialization methods for every class manually. Another huge drawback of XML serialization is that it does not handle references well.
( http://en.wikipedia.org/wiki/Encapsulation_(object-oriented_programming) )

That left binary serialization, which in the basic case only requires that a [Serialization] attribute be attached to the type to be saved. Unlike XML, binary serialization will save and restore private properties and references, but the save files are impossible to read manually. Initially, this appeared to be a good solution that would not require too much extra work.

Time to break out the hex editor!

As mentioned in part III of this dev diary, I am extensively using a design pattern called FatEnum to bring java-style enum functionality to C#. One of the key aspects of that design pattern is implementing the enum values as static instances. This allows fast and easy comparisons, such as this:
if(myValue == Planet.Mars){
    //build colony
}
( http://artificerentertainment.blogspot.ca/2014/05/guildcraft-dev-diary-part-iii.html )

It also creates a big problem with serialization. Normally, when an object is saved, all that is expected of it is that its data fields are restored when the file is loaded into a new instance of that type. For most cases, this makes a lot of sense. In the case of FatEnum it does not. That approach would create multiple instances of the same enum! Fast reference comparisons are no longer possible, and suddenly there are multiple copies of the data in memory.

Aside: Although saving some RAM is great, the main reason this is not an acceptable solution is that FatEnums can be grouped via interfaces. Using the equality operator (==) would suddenly return inconsistent results when comparing FatEnums that were deserialized.  
This problem can be avoided, but requires strict use of .Equals() instead of the equality operator. I am not fond of silent errors that are difficult to track down. It is possible that using the new Roslyn C# compiler extensions, the compiler could point out these mistakes and eliminate the need for what I write below. (Assuming that the visual difference between a==b and a.Equals(b) won't bother you.)

The real goal is not to create a copy when deserializing a FatEnum. It is to locate the associated static instance and assign it to the 'this' pointer of the enum that is deserializing. The problem with that goal quickly becomes apparent. (It is impossible in C# to reassign the value of the 'this' pointer.) The only option is for an outside class to modify the enum reference that is being deserialized.

It still makes sense for the FatEnum class to serialize itself. That way, each class that holds a reference to a FatEnum does not need to re-implement serialization of it. Once some identifying information has been temporarily assigned to the deserialized FatEnum, a "second pass" is performed by assigning the [OnDeserialized] attribute to a method in the parent class. The second pass is performed by calling a helper method that uses reflection to identify and 'fix' all fields, properties, and containers that hold a FatEnum reference.

Boiler-plate code to fix deserialized FatEnum references.

I consider this minimal boiler-plate code to be an acceptable trade-off. Since deserialization errors can be difficult to detect, FatEnum throws an exception if its data is accessed before it is properly fixed. This is done by only saving and restoring a minimal amount of identifying information.

Aside: SortedSet in the .NET framework does not properly serialize containers of user-defined types the way that List does.This led to me removing it from my implementation of EnumSet.

The next feature I will look at will be a grid for combat. I have decided to review a Unity plugin called Grids 1.8.
( http://gamelogic.co.za/grids/features/ )


Jim



No comments:

Post a Comment