Monthly Archives: March 2012

Analysing the first prototype

The past weeks I have been working on working on some general gameplay functionality for the main project, in addition to further integration of the first prototype of my persistence layer. It is now possible to store the game state in a save slot on a server. I have also analyzed the current strengths and weaknesses of the system. The following diagram shows how it is designed:

The most important parts are the IEntity interface which is implemented by game entities and enables them to mark fields to be persisted and to receive signals after loading and other persistence events. The bulk of the work is handled in the implementations of IStreamEncoder, of which currently only XmlEncoder is implemented and uses reflection and the XmlWriter and XmlSerializer classes to serialize the entities and their fields to an xml document. Which can be saved to a file, or somewhere else, by the IPersister.

I described the relevant problems and my chosen solutions in my previous post so I will not go into those here and now. I was mostly on the right track, and the overall system is working fine, though there are still quite a few points of improvement.

Strengths

  • Seperation of responsibility: Serialization and storage details are abstracted away from the rest of the game logic.
  • Simple and Intuitive: Adding extra persistent game entities is easily done by implementing IEntity and marking the relevant fields and properties with the PersistField attribute.
  • Convenient: It is possible to persist fields defined in an object related or attach to the entity by wrapping them in a persistent property.
  • Flexible: different implementations of IPersister can easily be switched out to change the overall behaviour of the system.

Weaknesses

  • Dynamic entities: The current system collects all entities in the scene during saving, and extracts their data. Upon loading, all entities in the scene are again collected, and filled with the data that was saved earlier. This works for scenarios where the state of entities changes, but it does not work in scenarios where entities have to be created, or removed to reach the saved state. We used a workaround in the form of a manager entity to handle the state of a variable number of other game elements. Recreating entities at the right place in the scene and hierarchy would be desirable. This also will require a way to instantiate game entities without needing references to prefabs.
  • Dependencies: Some entities turned out to have dependencies with others and expected those to have been initialized already. I quickly solved this by adding a priority to IEntity to govern the order in which OnAfterLoad is called on them. This is perhaps not the best solution. Priority based on location in the scene hierarchy might be an intuitive way to handle this, but is less flexible. Food for thought.
  • Integration: Integrating with existing systems turned out to be a bit trickier than expected. The save methods do not take parameters so options like save names needed to be set in the current IPersister while ideally, only the IPersistenceManager should be accessed once the system is configured. This part of the design could be reworked.

Other Comments

  • IEntity: This interface now contains four methods implemented by entities to receive signals when needed on persistence events. These should be optional but the interface requires an implementation, which can be irritating. This could be changed.
  • Design: As of this moment the bulk of the work happens in the XmlEncoder, which will require duplicating functionality when different encoders are desired. The extraction of data and serialization of data should be separated.
  • IEntity: My colleague offered another suggestion to handle the extraction problem. Instead of code annotations, this would involve an object with a list of names of fields to be persisted, which could be attached to a game entity. In Unity this could be a component, granting the user the ability to set values to save in the inspector. It is an interesting idea which merits further investigation.

Conclusion

The current prototype is quite successful. It does the job and does it reliably, and has kept the persistence implementation of most game entities relatively simple. It has proven that the chosen solutions work for this problem. There are still aspects where it falls short, which should be addressed in the following iterations of new prototypes. Handling dynamic game entities and dependencies should have the highest priority right now.

The First Prototype

As planned, I have been working on my first prototype for my persistence layer. This prototype will be integrated into a current project with a deadline so it needs to be simple.

I have chosen the most promising methods for dealing with each sub-problem which I have identified in my earlier preliminary research.

Extraction

By extraction I mean the method by which the relevant data is collected and extracted from the current state of the game. I found three candidate solutions to solving this:

  1. Domain model: Have all game objects contain references to separate data objects.
  2. Reflection: Have all game objects use annotations to mark data they want serialized, and extract using reflection.
  3. Manifest: Keep all data separate in a single object graph, commonly called a manifest

I decided that reflection is the most promising candidate for the first prototype, since it would enable a lot of flexibility and not require a major restructuring of the already existing code base. It results in a slightly more complicated persistence layer but is offset by the ease of development for the user. It was inspired by the way object relational mapping is commonly used in Java and to a lesser extend .NET systems.

The domain model and manifest solutions would result in a simpler persistence layer, but would require a lot of changes in the existing code base to supply the relevant data.

Serialization

I am working in Unity, which means I have access to most functionality in .NET implemented by Mono, including serialization.

  1. XmlWriter/XmlSerializer: serialize to XML
  2. BinaryFormatter: serialize to binary stream

The choice of serialization method to use is sometimes very dependent on the situation, but usually comes down to preference. In this case I was given to understand that the data would be sent to a server later, so encoding to text was recommended. Xml was the logical solution to use in this case.

Storage

During testing, storing data on the filesystem makes analyzing it and debugging easier, but later the data should be sent to a web server connected to a database. I decided to make the system flexible by creating interchangeable modules to change the behavior when needed. I made one module to save to the filesystem using a FileStream and another to interface with a different system handling the remote server.

Reconstruction

To prepare for reconstruction, I made sure to structure the serialized data in such a way as to be able to keep it apart later and still know where everything belongs. This is why I gave every entity a unique identifier. This allows the system to restore all the data to where it belongs. The actual restoring of the scene can by a combination of the following methods:

  1. Automatically save and restore entire scene hierarchy related to the entity
  2. Save entity and use custom methods per entity for restoration after loading the data.
  3. For dynamic entities that should be instantiated, the factory pattern is a good approach.

I decided to go for the second option for two reasons. The most important reason is that option one would make the persistence layer very complex, and I do not have the time right now to implement such a solution, while the sacrifice in usability is very small. Each entity can easily handle its own initialization after being loaded with data by implementing OnAfterLoad and other methods. The other reason is that the first option might lead to a lot of redundant data, which might create storage issues and would introduce a lot of places where things could go horribly wrong.

To summarize

I have chosen the best solutions for the sub-problems identified and started implementing the first prototype. Initial results seem very promising. The prototype has already been integrated into the main project to have actual data to save and it is performing nicely. There are still a few kinks to work out, and a few glaring shortcomings, but it will serve for the current purposes.