Simplest Dictionary Serialization to a File?

What’s the simplest way to save/load a dictionary to/from a file, bypassing Unity’s built-in serialization? Specifically, I’d like my data to persist while switching from editor mode to player mode and back.

Dictionaries serialization/exposure along with many other types is what VFW offers, best of all it’s free. All your custom data is serialized and persist during assembly reloads.

Saving something to file is a walk in the park!

public class SaveMyDic : BetterBehaviour
{
	public Dictionary<string, List<int>> myDict = new Dictionary<string, List<int>>();
	public string fileName = "dict.bin";

	string dictPath { get { return Application.dataPath + "/" + fileName; } }

	[Show] void Save()
	{
		string serializedData = Serializer.Serialize(myDict);
		using (var writer = new StreamWriter(File.Open(dictPath, FileMode.OpenOrCreate)))
		{
			writer.WriteLine(serializedData);
		}
	}

	[Show] void Load()
	{
		using (var reader = new StreamReader(File.OpenRead(dictPath)))
		{
			string serializedData = reader.ReadLine();
			myDict = Serializer.Deserialize<Dictionary<string, List<int>>>(serializedData);
		}
	}
}

As detailed in this answer, I was having a little too much trouble saving/loading the dictionary data to/from lists using Unity’s built-in serializer. Other people use this method routinely it seems, but by my understanding the unity-serialized data survives going from Editor mode to Player mode but not viceversa. I therefore decided to bypass unity’s serialization completely.

As such, here is a script that no matter how unrealistically simple, inherits from MonoBehaviour and allows the data in the dictionary to survive switching from Editor mode to Player mode and viceversa. I post it here, with its considerable limitations, in the hope it might provide a starting point for others.

The main class follows. To use it, just drag and drop it onto the components list of a GameObject, i.e. a default cube. Notice that, strictly speaking, it is not the simplest possible code as it includes some demo functionality to populate the dictionary. You might want to replace that code with something closer to your game’s usage scenarios.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;

[ExecuteInEditMode]
public class PersistentDictionaryExample : MonoBehaviour 
{
	private Dictionary<string, string> theDictionary;
	private string prefix;
	
	private static string savedDataPath = Application.persistentDataPath + "/savedData";
	private static string dictionaryName = "PersistentDictionaryExample.txt";
	private static string dictionaryFullName = savedDataPath + "/" + dictionaryName;
	
	private string checkMode()
	{
		if(Application.isPlaying)
			return "PlayMode::";
		else
			return "EditMode::";
	}
	
	// in OnEnable() we load the content of the dictionary from file
	void OnEnable() 
	{

		theDictionary = new Dictionary<string, string>();
		
		// CreateDirectory() checks for existence and 
		// automagically creates the directory if necessary
		Directory.CreateDirectory(savedDataPath);
		
		// the file is a simple key,value list, one dictionary item per line
		if(File.Exists(dictionaryFullName))
		{
			string[] fileContent = File.ReadAllLines(dictionaryFullName);
			
			foreach(string line in fileContent)
			{
				string[] buffer = line.Split(',');
				if(buffer.Length == 2)
					theDictionary.Add(buffer[0], buffer[1]);
			}
			
			Debug.Log(checkMode()+"Awake()::LOADED DATA from "+dictionaryFullName);
		}		
	}
	
	// in OnDisable() we save the content of the dictionary to file.
	public void OnDisable()
	{

		if(theDictionary != null)
		{
			string fileContent = "";
			
			foreach(var item in theDictionary)
			{
				fileContent += item.Key + "," + item.Value + "

";
}

			File.WriteAllText(dictionaryFullName, fileContent);
			
			Debug.Log(checkMode()+"OnDisable()::SAVED DATA in "+dictionaryFullName);
		}		
		
		theDictionary = null;
	}
	
	// to simulate modifications to the dictionary in play mode
	// we simply keep count of the number of update calls since 
	// this object was created. In Edit mode the addRandomContent
	// method is triggered manually by the user, via the inspector.
	void Update() 
	{
		if(Application.isPlaying)
		{
			string key = "updates";
			
			if(!theDictionary.ContainsKey(key))
				theDictionary[key] = "0";
			
			theDictionary[key] = (int.Parse(theDictionary[key]) + 1) + "";	
		}		
	}
	
	// this method is called manually by the user in Editor mode, 
	// through the identically-labelled inspector button. 
	// Along with the Update() method it is used to populate/modify 
	// the dictionary with simple data.
	public void addRandomContent()
	{
		// GetRandomFileName returns a 8.3-characters filename
		string aKey = Path.GetRandomFileName();
		string aValue = Path.GetRandomFileName();
		
		// the dot is removed to obtain a simple 11-characters random string
		aKey = aKey.Replace(".", "");
		aValue = aValue.Replace(".", ""); 
		
		theDictionary.Add(aKey, aValue);	
	}
	
	// not strictly necessary but useful to conveniently list 
	// the content of the dictionary in the inspector
	public Dictionary<string, string>.Enumerator GetEnumerator() 
	{
		if(theDictionary != null)
			return theDictionary.GetEnumerator();
		else 
			return (new Dictionary<string, string>()).GetEnumerator();
	}

}

Some important details: ideally I would place the saving functionality in the OnDestroy() call. In a class supposed to work only in Player mode that would probably work. Unfortunately, as detailed in this answer, while switching from Editor to Player mode, OnDestroy() is invoked after an OnEnable()/OnDisable() “combo” (perhaps part of a recompilation step) that destroys the dictionary. As such I was left with no choice but save the data in OnDisable() as it is invoked once before the combo.

Alongside the main class is a small inspector, to be placed in Assets/Editor, to keep an eye on the content of the dictionary and manually populate it with some random data.

using UnityEngine;
using UnityEditor;
using System.Collections;

[ExecuteInEditMode]
[CustomEditor (typeof(PersistentDictionaryExample))]
public class PersistentDictionaryExampleInspector : Editor 
{
	private PersistentDictionaryExample theDictionary;
	
	void Awake()
	{
		theDictionary = (PersistentDictionaryExample) target;	
	}
	
	public override void OnInspectorGUI() 
	{
		GUILayout.BeginVertical();
		
	        if(GUILayout.Button("Add Random Pair"))
			{
				theDictionary.addRandomContent();
			}
	
			GUILayout.BeginHorizontal();
				EditorGUILayout.LabelField("keys");
				EditorGUILayout.LabelField("values");
			GUILayout.EndHorizontal();
		
			EditorGUILayout.Separator();
		
			foreach(var item in theDictionary)
			{
				GUILayout.BeginHorizontal();
				
					EditorGUILayout.LabelField(item.Key);
					EditorGUILayout.LabelField(item.Value);
			
				GUILayout.EndHorizontal();
			}
		
		GUILayout.EndVertical();
    }
}

Pitfalls

Obviously, the PersistentDictionary class is very limited, it’s only meant to be a simple example. Apart from the lack of proper file I/O checks and error handling, one of its biggest problem from my point of view is that it can’t be used on multiple objects as they’d all write to the same file. Short of writing one file per object using some kind of id to track the relationship in the filename, some kind of I/O data manager might be needed. I.e. the data could actually be on a single data manager while being accessed by the scripts/inspectors on individual objects. Alternatively, the data could be on the individual objects after all, loaded from the data manager in OnEnable() and stored back in OnDisable(). The data manager in both cases would then be responsible for the actual file I/O rather than the individual objects.

Comments and feedback are appreciated.