MMA Manager has a lot of data, for example when the game start and the world is generated we create fight promotions and populate them with almost 1000 fighters, and this is before any of the fighters the player manages are added to the game. These are the fighters the player’s fighters will face in the cage, and we simulate events for every league every month as well so they fight each other too. We need to know a lot about each of these fighters for the simulation to work the way we want it to, like I said we have a lot of data and the data for the fighters is the largest chunk.
This post is really about saving in Unity & C#.
We started off using the standard C# serialiser with XML serialisation. This has its advantages, mainly a human readable save file, but was slow and produced a large save file. Running on a Mac it was fine, but on an iPhone it produced a noticeable pause. When I first profiled our save was taking close to a 1.5 seconds and producing a 2MB save file, opening it up it was over 42,000 lines of XML!
Making It Faster & Smaller
The first step was obvious, goodbye XML serialisation, hello Binary Serialisation. This was an instant improvement, the save was both smaller and quicker, however, it wasn’t really small enough or quick enough yet. The initial save after world creation was still over 300KB and took over half a second. The initial save is also the smallest as the amount of data needing stored grows as the player hires fighters and coaches.
The second step was to take a closer look at what we were actually saving.
For example, fighter names are made up of three parts (forename, nickname, and surname) we were saving the strings for these, but these names are randomly picked from tables so we only need to save the index. Three ints instead of three strings, this saved us 30KB on the initial save. Since that change we’ve added the ability for the player to change a fighter’s nickname, so we’re having to store a string again but with all the other changes it’s not a concern.
Instead of saving fighter portraits, which are generated from multiple parts, I saved the random seed used to generate that portrait. This saved 60KB on the initial save.
I also went through and made sure we were only saving what we needed, so variables which could be lazily evaluated and temp lists (instead of creating and destroying a list every time a function is called have a temp on in the class to use, cuts down on garbage collection) were excluded from the save.
Lastly I took a look at some data structures. For example, our Stats class was using a list of pairs (we have our own pair implementation, coming from the C++ world we like pairs), I changed it to an array of floats and that got us 4KB in the initial save, only the four prospects you can hire have stats in that initial save, the 900+ other fighters generate their stats the first time they need them. But that’s a 1KB saving per fighter & coach that needs stats saved, that’ll add up over time.
By the time all these changed were made the initial save size was down to 197KB and took about 400ms on the iphone. Still too slow, but it’ll do for now.
And then…
All that was done in mid May, a couple of weeks ago we started sending out Test Flight builds to some of our friends and one of them had an issue with the save breaking. Looking at the save file it had stopped writing part way through, this prompted another look at our save system last week.
Do It Right This Time
The save was clearly still too big and too slow. So I bit the bullet and did it the good old fashioned way and wrote a custom save and load function for every class in the game that needs to save data.
Let’s start with the result of this for the initial save (these timing come from my Mac using System.Diagnostic.Stopwatch), this is what I sent Kieran in Slack:
Old: save 82ms, load 87ms, 197kb
New: save 10ms, load 12ms, 82kb
the end month saves take between 4 & 10 ms
It’s faster, it’s smaller, and it gives us complete control over what we’re saving. On the iPhone it doesn’t show up on the profiler at all any more.
It’s how save/load has been written on every game I’ve every written before I worked with Unity, be it games I’d written on my own before I ever worked in the games industry or the games I’d worked on in my years at Lionhead. So why not do it this way to start with?
Convenience, simple as that. We’re a two man team, so if we can take advantage of the general serialisation then why not, it’s one less thing for us to have to deal with. For many games this won’t be an issue and the general method will work just fine, but we ran into the worse possible combination of a large dataset and a relatively slow platform so for us it is the answer.
Example Code
This is last so you can ignore it if you want. What I do is create a BinaryWriter/BinaryReader depending on whether we’re saving or loading and pass it through the functions. The code speaks for itself so here’s a toy example:
public void Save(ref BinaryWriter bw)
{
bw.Write(m_Thing1);
bw.Write(m_Thing2);
bw.Write(m_List.Count);
for(int i = 0; i < m_List1.Count; ++i)
{
bw.Write(m_List[i].Var1);
bw.Write(m_List[i].Var2);
}
bw.Write(m_Array.Length);
for(int i = 0; i < m_Array.Length; ++i)
{
m_Array[i].Save(ref bw);
}
}
public void Load(ref BinaryReader br)
{
m_Thing1 = br.ReadInt32();
m_Thing2 = br.ReadSingle();
int count = br.ReadInt32();
for(int i = 0; i < count; ++i)
{
Foo foo = new Foo();
foo.Var1 = br.ReadInt32();
foo.Var2 = br.ReadBoolean();
m_List.Add(foo);
}
count = br.ReadInt32();
if(count > 0)
{
m_Array = new Bar[count];
for(int i = 0; i < count; ++i)
{
m_Array[i] = new Bar();
m_Array[i].Load(ref br);
}
}
}