How I implemented save/load in WoT: A Serialization Nightmare
In every game dev project there comes a time when you have to face a technical challenge you don't like at all. For some people it's AI, for some others it's pathfinding and for me it's serialization. Let me explain a bit: I come from a web back-end development background, so persisting stuff is, by now, part of my very nature. But working for almost 10 years with databases (relational or otherwise) saving the whole state of a game in a single file via serialization sounds like a nightmare to me.
That worked like a charm almost immediately, even though I had to do some refactoring in order to make things a bit tidier and make sure every single class I had in my model could be serialized easily and contained to references to things I did not want to persist.
Implementing the load game feature wasn't hard after this. It was just a matter of putting all of those objects in the places where they belong and instantiate prefabs for cities and ships.
And hello:
So, now when the user loads a game I simply run the terrain generation algorithm again using the world seed and then populate it with the cities, ships, companies and all of the things that make the game, well, a game.
Finally! After weeks of work, I can save the game and resume it later! |
The first approach
Naturally, the very first thing I did was take a look at the C# serialization classes and hope that everything would be solved immediately. After reading some tutorials and articles about it, it sounded like the perfect solution.
That did not last, though. I quickly ran into issues, though: It seems that some of the Unity classes that we hold near and dear to our hearts - like Vector3, Quaternion and Color - do not implement the Serializable attribute that C# requires in order to be able to work its magic. It also turns out that Unity really dislikes serialization cycles, and my model classes are full of those.
A quick example:
A City may have a Shipyard that has a backlog of several ShipBeingBuilt where each one belongs to a Company who has in its power a list with PriceInformation of every City. Yikes!
That did not last, though. I quickly ran into issues, though: It seems that some of the Unity classes that we hold near and dear to our hearts - like Vector3, Quaternion and Color - do not implement the Serializable attribute that C# requires in order to be able to work its magic. It also turns out that Unity really dislikes serialization cycles, and my model classes are full of those.
A quick example:
A City may have a Shipyard that has a backlog of several ShipBeingBuilt where each one belongs to a Company who has in its power a list with PriceInformation of every City. Yikes!
Going in the right direction
After deciding that the built-in serialization tools were going to be a pain in the butt and require lots of changes in order to solve my problem I started taking a look at third-party tools. I quickly came across this one, that is, honestly, a masterpiece: Unity Level Serializer.
What does Unity Level Serializer do? Exactly what the name suggests: it takes all of the entities that you mark with the StoreInformation component and persists them, including their components, their meshes, their materials, their animations and anything else you can possibly imagine.
So I immediately started adding the StoreInformation component to all of my cities and ships, but that didn't prove to be very effective: the resulting saved game file was pretty big and it took the serializer a long time to go through all of my objects. I was definitely saving a lot of data I didn't need: to be honest, I only need to preserve my model data (ships, cities, companies, and so on) in order to reconstruct the game status after loading.
Almost there!
So, it looked like it was time to do some micromanagement on the way the UnitySerializer persists stuff. I went ahead and changed things: I removed the StoreInformation component from all of my objects and created a new script (cleverly named SavedGame) to contain all of the information I needed to restore the game status upon loading. It looked something like this:
import System.Collections.Generic;
public var currentDate : System.DateTime;
public var playerCompany : Company;
public var aiPlayers : List.<AIPlayer> = new List.<AIPlayer>();
public var citiesData : List.<City> = new List.<City>();
public var terrainHeightData : float[,];
public var terrainAlphaMap : float[,,];
public var terrainTrees : TreeInstance[];
...
public var windDirection = 0.0f;
public var windSpeed = 0.0f;
public var currentDate : System.DateTime;
public var playerCompany : Company;
public var aiPlayers : List.<AIPlayer> = new List.<AIPlayer>();
public var citiesData : List.<City> = new List.<City>();
public var terrainHeightData : float[,];
public var terrainAlphaMap : float[,,];
public var terrainTrees : TreeInstance[];
...
public var windDirection = 0.0f;
public var windSpeed = 0.0f;
That worked like a charm almost immediately, even though I had to do some refactoring in order to make things a bit tidier and make sure every single class I had in my model could be serialized easily and contained to references to things I did not want to persist.
Implementing the load game feature wasn't hard after this. It was just a matter of putting all of those objects in the places where they belong and instantiate prefabs for cities and ships.
Terrain data can take a lot of space
First thing I noticed after having this working was that it was taking up a lot of disk space, something like 250 megabytes. I'd say that, even though storage is incredibly cheap and abundant nowadays, it's unacceptable for a saved game to take so much space.
The culprits were easy to identify: I was saving all of the terrain height data, ground texture alpha maps and the position, type and color of every single tree in the world. The solution was quite easy, though: replace those 200+ megabytes of data with a single int: the seed that the terrain generator used in order to create the world.
Basically, goodbye:
The culprits were easy to identify: I was saving all of the terrain height data, ground texture alpha maps and the position, type and color of every single tree in the world. The solution was quite easy, though: replace those 200+ megabytes of data with a single int: the seed that the terrain generator used in order to create the world.
Basically, goodbye:
public var terrainHeightData : float[,];
public var terrainAlphaMap : float[,,];
public var terrainTrees : TreeInstance[];
And hello:
public var worldSeed : int;
So, now when the user loads a game I simply run the terrain generation algorithm again using the world seed and then populate it with the cities, ships, companies and all of the things that make the game, well, a game.
Parting thoughts
It looks all so simple now that it's done, but it was an almost three week long nightmare fighting against serialization. There are a few things that I could have done better in order to make things easier for future (now past) me, though:
- Make sure that the data that's important for your game is separated from presentation stuff. Basically, separation between view and model (very important practice in web development that I, foolishly, didn't apply when coding this game). I had to do some refactoring because I had made the City class (that has attributes like name, population, goods, shipyard and so on) as a MonoBehaviour that also contained things like the different meshes the city can have depending on the population and stuff like that. Do I want to persist the population of a city? Definitely. Do I want to persist every polygon the city mesh uses when its population is 34,000? Not at all, I already have that information in the game itself!
- If you plan to persist the game state at some point, take that into consideration from the very beginning. For example, making the whole world be generated based on a seed is a good choice so you can then persist just that magical number and you're all set.
- Be very careful with the relationships between your classes or you might end up persisting a lot of stuff you didn't mean to. Perhaps you have a City class that references your Ship class that references a PathfindingHelper class that references the Terrain and you end up serializing the color of each leaf of every tree in the whole world! Unity Level Serializer provides a very easy to use annotation to mask those things you don't need to persist: @DoNotSerialize.
- Start small and build up from there. This pretty much applies to software engineering as a whole, but it's really important to remember regarding serialization. Those times when I tried to persist everything at once I ended up under a sea of exceptions and about to have a nervous breakdown!
Comentarios
Publicar un comentario