Since we are working on a live action RPG and really want to emulate as much of the feel as you would get playing inside an MMO world like EQ or WoW it is obvious we are going to need an easy and flexible way to save state data for each object in each scene. The first thing I needed to figure out was when these saves should happen. We know we need to save the states for all objects anytime we are going to be leaving the scene and these objects are going to be unloaded; this tells us we basically need to save on scene transitions and when the player closes the game.
The next thing we need is a unique way to identify each object in the scene that will be persistent between each load. The initial thought I had was to use a GUID that is generator in the editor when an object is created but it appears the .NET Guid is not implemented correctly in the iPhone version of Unity. This meant that I needed to find a quick and easy way to generate a unique id for each object, which led me to the method below.
private static ulong m_next_safe_index = 1;
/// <summary>
/// Creates a unique hash ID based on a seed string and computed using current
/// time and other random aspects.
/// </summary>
/// <param name="seed">What you would like to seed the random name with</param>
/// <returns></returns>
public static string CreateUniqueID(string seed)
{
byte[] plainText, rawHash;
using(MemoryStream pbs = new MemoryStream())
{
using(BinaryWriter bw = new BinaryWriter(pbs))
{
bw.Write(seed);
bw.Write(System.DateTime.Now.Ticks);
bw.Write(Random.value);
bw.Write(m_next_safe_index++);
bw.Write(seed);
}
plainText = pbs.ToArray();
}
rawHash = new MD5CryptoServiceProvider().ComputeHash(plainText);
return System.Convert.ToBase64String(rawHash).Replace('+', '-').Replace('/', '_');
}
The first thing to note about the above method is that it will NOT create a truly unique id each time; but the probability of it generating the same id during the life of this project is very slim. It basically uses the current ticks of the time component to make sure the value is unique between invocations, in the case where we call it multiple times in the same frame (where the ticks could be the same) we also use an incrementing variable. The seed wrapper is just an added layer of insurance to prevent objects of different types from sharing the same id space.
Now that we have a unique id to work with we can figure out how to invoke our save. In Unity this is very simple, since all of the objects that need to have their states saved are scripts we can use the GameObject.FindObjectsOfType() method to track down all our savable scripts. This method will take a base class or interface and return all objects which are assignable from that class.
The next step is to create our base savable object structure. This structure will probably be tailored to whatever type of game you are creating, in my case I created a very flexible base class which I could easily extend for more complicated objects. You can see the base class I used below:
using System.IO;
using UnityEngine;
public abstract class SavableBehaviour : MonoBehaviour
{
public string UniqueID = SaveManager.CreateUniqueID("SN");
public abstract void SaveState(BinaryWriter bw);
public abstract void ReadState(BinaryReader br, float elapsed);
protected virtual void OnDrawGizmosSelected()
{
Object[] savables = FindObjectsOfType(typeof(SavableBehaviour));
foreach (SavableBehaviour sb in savables)
{
if (sb == this) continue;
if (sb.UniqueID == this.UniqueID)
{
UniqueID = SaveManager.CreateUniqueID("SN");
break;
}
}
}
}
Notice that the above class extends the Unity MonoBehaviour class. The reason I wanted to do this instead of using an interface is because of the method I use for finding my savable objects. It will force me to only use MonoBehaviour classes which can be returned by the FindObjectsOfType() method I am using. Also note that the ReadState() method receives an elapsed parameter. This is so that each object can simulate the amount of time which has passed since the scene was saved, it is very useful for managing rare boss spawns.
There are still a lot of kinks to work out with my saving system, but for 2 days worth of work I think it is coming together very nicely. One issue that has driven me crazy is the need to create a new unique id when you duplicate on of these objects in the editor. Since we use a public field to store our id so it gets auto-serialized with the scene it is also copied when we duplicate the object. I am looking for a good solution for this, the temporary solution is to hack up the OnDrawGizmosSelected() so that it changes the id of the newly duplicated object. It seems to be working fine for right now but it makes me anxious having code like this that is a bit unpredictable.