Pages

Saturday, November 14, 2020

Ancient Star - serialization

The nice convention about mobile applications is that you can turn them "off" at any moment and continue where you've left off later. Yeah, some apps don't work that way but developers are generally encouraged to make apps tolerant to being paused and resumed at any moment. In order to pull this off in the Ancient Star I need a solid system for saving and loading game data.

There are other things to do to make an app lifecycle aware (handle pausing, stopping, and resuming) but Android SDK does a very good job of covering the usual parts. Built-in UI components take care of themselves, custom components (like one where galaxy map is drawn) are fairly easy to make persistent and it's easy to move data around in a lifecycle aware manner. But in comparison getting game data into a format that can be written to a file and back is significantly more work. You might be fooled by built-in JSON converter and open source solutions that this is an already solved problem but I haven't found a solution with both low maintenance overhead and the ability to cope with complex data structures.

When serializing data there are two parts of the process, picking up data from objects and converting data. Think of it as answering "what" and "how" questions. Serializers like built-in Android one expect from app developer to answer "what" question so the code ends up handling the same piece of an object in three different places: declaration, serialization, and deserialization. This incurs considerable code maintenance overhead. Whenever you add a new data member to an object, you have to remember to include it in serialization and deserialization functions. Forgetting to do so is still legal code, the compiler will not complain about it and the app might run for a good while until the issue becomes apparent. And even then it's not trivial to figure out which member did have you forgotten to serialize or deserialize. Better libraries ask you only to put a special annotation next to member declaration so it is very easy to keep serialization coverage in sync with the actual object structure.

As long the structure is simple (tree). Such serializers work by dereferencing each reference and embedding values behind it inside the object that held the reference. So if there is an object referenced by multiple objects its data will get serialized multiple times, producing unnecessary data duplication in a save file, and what is worse, deserialization would not be able to deduplicate it, potentially producing a faulty game state. Cyclical references are even worse, they'll make a serializer run in an infinite loop. And it's easy to have data with such structures. For instance, in the Ancient Star, each Star object can have a reference to a Colony object, Colony has a reference to a Player who owns it and the Player has a list of scouted Stars. That's an example of both a cycle and multiple references to the same Player object since each colony references its owner. It is possible to swap direct references with indirect ones and make data simpler in the eyes of a serializer but it comes with the price of more complicated code. And besides, in the Stareater I did develop a serializer that both utilizes annotations and can work with complex reference networks.

The first "trick" I used there is to have the serializer make indirect references out of real ones. It generates a unique "name" for each object and references are substituted with those names. The second "trick" was to gradually build a reference network while deserializing, similar to how in the normal course of the game the complexity of data gradually increases. For instance, initially, stars don't have colonies, are not scouted, and during the game each player scouts and colonize more and more stars. Similarly, the deserializer can first create objects with only bare necessary data (primitives and references required by a constructor) and fill them with remaining data in the second pass. So how long it took to port the code from the Stareater? 30% of the total development time so far!

I expected it would take some time because I wanted to learn and use Java's annotation processor stack for code generation but I expected it to take half as much. Stareater is written in C# and runs on .Net (CRL actually) while Ancient Star runs in Java Virtual Machine (or something close to it). Certain metadata (generic parameters) which is available in CRL is not available in JVM and Stareater code depends on it. I didn't know that until I ported most of the code and give it a spin. I had to scrap and rebuild the serialization code two more times until I arrived at a workable solution. It was a frustrating road but I'm glad I have done it. It will pay off in the future.

No comments:

Post a Comment