Letâs suppose youâre making a âcompleteâ game. You want a title screen and everything. Your user can select the number of players or a difficulty setting, etc, and then⌠how do you pass the information on to the next scene? Or more importantly (and somewhat tricker) how can you save a game session and load it again later? Read along and I will show some of the options I like.
Static Persistance
To persist data from one scene to another can be as simple as saving data to a static variable. To demonstrate, we will need a bit of setup. Create two scenes, one called âTitleâ and one called âGameâ. From the menu bar choose âFile->Build Settingsâ and in the dialog you will need to use the âAdd Currentâ button or drag and drop both scenes from the Project pane into the âScenes In Buildâ list.
The following Data Manager class is a static class with a static variable, so it doesnât need to be added to your scene, it will just âworkâ automatically. I make use of something called an âenumâ which I havenât talked about before. For now, you can think of an enum as an integer (because it can be cast to or from an integer – although it is actually its own âtypeâ) where each entry is named to make your code more readable. âEasyâ is like the integer value of â0â and it counts up from there.
using UnityEngine;
using System.Collections;
public enum Difficulties
{
Easy,
Medium,
Hard,
Count
}
public static class DataManager
{
public static Difficulties difficulty;
}
Add the following âTitleControllerâ script as a component to the camera in the âTitleâ scene. This script is very simple – I used the legacy GUI to reduce setup time even further. All this script does is show buttons for each difficulty option, and one to begin playing the game. Whenever you select one of the buttons, a difficulty setting is set to a static variable in the DataManager class.
using UnityEngine;
using System.Collections;
public class TitleController : MonoBehaviour
{
void OnGUI ()
{
int yPos = 10;
GUI.Label(new Rect(10, yPos, 200, 30), "Choose Difficulty");
for (int i = 0; i < (int)Difficulties.Count; ++i)
{
yPos += 40;
Difficulties type = (Difficulties)i;
if (DataManager.difficulty == type)
GUI.Label(new Rect(10, yPos, 200, 30), type.ToString());
else if (GUI.Button(new Rect(10, yPos, 100, 30), type.ToString()))
DataManager.difficulty = type;
}
yPos += 40;
if (GUI.Button(new Rect(10, yPos, 100, 30), "Play"))
Application.LoadLevel("Game");
}
}
Add the following âGameControllerâ script as a component to the camera in the âGameâ scene. This script is also very simple. This time all I do is show which difficulty had been selected in the Title scene, and provide an option to Quit back to that scene.
using UnityEngine;
using System.Collections;
public class GameController : MonoBehaviour
{
void OnGUI ()
{
GUI.Label(new Rect(10, 10, 200, 30), DataManager.difficulty.ToString());
if (GUI.Button(new Rect(10, 50, 100, 30), "Quit"))
Application.LoadLevel("Title");
}
}
With the Title scene open, press Play and note that the selection you make on the first screen is saved and is still available in the next scene. This works because static variables never go out of scope for as long as the program is running.
Singleton Pattern
Although a static class worked fine for the previous sample, it doesnât offer as many architectural options as an object would (namely inheritance and polymorphism). A singleton is a slightly different variation, where by design you still only want a single instance of a class to ever exist, but you can have more control over the creation and lifespan of the object, can use subclasses, etc.
public class DataManager
{
public static readonly DataManager instance = new DataManager();
private DataManager() {}
public Difficulties difficulty;
}
Here I have modified the DataManager class so that it is no longer static. I create a static readonly instance of the class (so that no other class can destroy or reassign it) and make the constructor private (so no other class can instantiate another object). This forces the singleton pattern to be used as I intended.
Reading and writing the difficulty setting would now need to be routed through the singleton instance. The following line shows an example of writing the value:
DataManager.instance.difficulty = type;
Unity âSingletonâ
Sometimes you may find it convenient to use a MonoBehaviour based class for your persistence (in case you want to take advantage of Coroutines, etc.) In that case you can make GameObjects âsurviveâ a scene change by using the method âDontDestroyOnLoadâ. Note that this is also a handy way to make music play between scene changes.
public class DataManager : MonoBehaviour
{
public static DataManager instance
{
get
{
if (_instance == null)
{
GameObject obj = new GameObject("Data Manager");
_instance = obj.AddComponent<DataManager>();
DontDestroyOnLoad(obj);
}
return _instance;
}
}
static DataManager _instance;
public Difficulties difficulty;
}
This version of the DataManager inherits from MonoBehaviour. The âSingletonâ is created by something called âLazy Loadingâ which means that as soon as any script calls the âinstanceâ property, the class will look at its âgetterâ and determine whether or not it has created one. If not, it will create a new GameObject, assign the correct component, and make sure that the GameObject is marked properly so it wonât be destroyed when changing scenes.
This version is not as âsafeâ as the previous version, because there are ways that other scripts could cause your scriptâs GameObject to be destroyed, and even though your script itself would âsurviveâ due to the static reference, the comparison to null will still return true because the equals operator is overridden by Unity (to take into account the GameObject), therefore another GameObject/Singleton would be created.
Furthermore, nothing stops another script from adding additional copies of this component to other GameObjects, although because I provided no setter, only one component at a time will ever be recognized as the main instance.
Player Prefs
All of the examples so far are good ways of taking data from one scene to another, however not a single one of them can persist data across multiple play sessions. In order to accomplish this task, you need to save data to âdiskâ in one way or another. Unity provides a convenient solution without even needing to understand how to create, read and write to files. This solution is called PlayerPrefs – see the docs here http://docs.unity3d.com/ScriptReference/PlayerPrefs.html
You can think of the PlayerPrefs as a Dictionary which only knows how to work with a âstringâ for the key and either an âintâ, âfloatâ, or âstringâ for the value. Unlike a generic dictionary, you can mix and match any combination of those values.
Here are some use cases:
// Store an integer value
PlayerPrefs.SetInt("Difficulty", 0);
// Retrieve an integer value (if it exists, or use a default of '0' otherwise)
DataManager.instance.difficulty = (Difficulties)PlayerPrefs.GetInt("Difficulty", 0);
Serialization
I mentioned serialization once early on – Unity utilizes serialization to store values of your components while in the editor. Unfortunately, a lot of the convenient options you might like to make use of such as a Binary or XML Serializer would require you to be able to use a Constructor – not an option with MonoBehaviour. Furthermore, object hierarchy, object references, and a desire to persist values in native Unity components all greatly complicate this process.
The method I prefer is manual serialization into JSON strings. Although it requires a bit more setup than some of the other routes, I feel that I have a lot more options and maintain full control over the process. I also donât have to worry about âversioningâ my data. I can remove data, add data, change data types, etc and it wonât cause the serializer to crash, because I can control the process of what and how I persist data. For example, I can check for the presence of keys and try casting data types where necessary. Additionally, because I am serializing to a JSON string I can very easily persist the result to PlayerPrefs, submit it to a server, or write it to a local file as desired.
I decided not to re-invent the wheel this time around, and used a public script for the JSON serialization instead. Grab a copy here, https://gist.github.com/darktable/1411710 and add it to your project.
Create a new scene called âDemoâ. Create a Cube (from the menu bar choose âGameObject->3D Object->Cubeâ) and then create and attach the following âMonsterâ script (because arenât monsters more fun to work with?)
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
public class Monster : MonoBehaviour
{
public int HP;
public virtual Dictionary<string, object> Save ()
{
Dictionary<string, object> data = new Dictionary<string, object>();
data.Add("HP", HP);
data.Add("X", transform.localPosition.x);
data.Add("Y", transform.localPosition.y);
data.Add("Z", transform.localPosition.z);
data.Add("Prefab", gameObject.name);
return data;
}
public virtual void Load (Dictionary<string, object> data)
{
HP = Convert.ToInt32( data["HP"] );
float x = Convert.ToSingle( data["X"] );
float y = Convert.ToSingle( data["Y"] );
float z = Convert.ToSingle( data["Z"] );
transform.localPosition = new Vector3(x, y, z);
}
}
This script has a single field for âHPâ – hit points. It doesnât really do anything, but I want to show that we will be able to save both a local variable as well as values from the transform component. The Save and Load methods serve (hopefully) obvious purposes. They wrap the objectâs data into a generic dictionary which the MiniJSON script knows how to serialize into a JSON string.
Create and attach the following script to the camera, and then connect the Cube as the reference for the monster field:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
public class Demo : MonoBehaviour
{
public Monster monster;
void Start ()
{
Load ();
}
void OnGUI ()
{
if (GUI.Button(new Rect(10, 10, 100, 30), "Randomize"))
Randomize ();
if (GUI.Button(new Rect(10, 50, 100, 30), "Save"))
Save ();
if (GUI.Button(new Rect(10, 90, 100, 30), "Load"))
Load ();
}
void Randomize ()
{
monster.HP = UnityEngine.Random.Range(10, 100);
float x = UnityEngine.Random.Range(-10f, 10f);
float y = UnityEngine.Random.Range(-10f, 10f);
float z = UnityEngine.Random.Range(-10f, 10f);
monster.transform.localPosition = new Vector3(x, y, z);
}
void Save ()
{
string json = Json.Serialize( monster.Save() );
Debug.Log(json);
PlayerPrefs.SetString( "Monster", json );
}
void Load ()
{
string json = PlayerPrefs.GetString( "Monster", string.Empty );
if (!string.IsNullOrEmpty(json))
{
var dict = Json.Deserialize(json) as Dictionary<string,object>;
monster.Load(dict);
}
}
}
This script uses the legacy GUI like before, to either move the monster around, Save its data, or Load its data. I use the MiniJSON script we downloaded for serialization, and then save the result to PlayerPrefs for persistence sake.
Run the scene. Click Randomize to your heartâs content. At some point click Save and then randomize a few more times. Now click Load and see that the monster is moved back to the location it was in when you clicked Save. Stop the scene and then run it again. The monster should still be in the place where you saved it!
Although you technically know everything you need at this point, a few more issues may not be obvious. For example, what if you have a list of objects you want to save? Do you need to create a new player pref for each one? What if you want to use polymorphism or dynamically create and persist objects? These are each very normal architectural requirements so letâs handle them next.
For our first step, we will need to modify the Monster script so that the Save and Load methods are marked as âvirtualâ. This way we donât have to completely re-write the Persistence code in every sub-class, instead, we only have to override the base method and append whatever new data the sub class provides.
I decided to create three subclasses of Monster: âBlueMonsterâ, âRedMonsterâ, and âGreenMonsterâ – I know, very imaginative class names right? The blue monster is listed below:
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
public class BlueMonster : Monster
{
public int water;
void Awake ()
{
water = UnityEngine.Random.Range(10, 100);
}
public override Dictionary<string, object> Save ()
{
Dictionary<string, object> data = base.Save ();
data.Add("Water", water);
return data;
}
public override void Load (Dictionary<string, object> data)
{
base.Load (data);
water = Convert.ToInt32( data["Water"] );
}
}
The red and green monsters have identical implementations, except that I named the local variable âfireâ and âearthâ in the red and green monster scripts respectively. The goal here is to illustrate that you canât use the same serialization across all three because they have âdifferentâ data sets. However, because they share a common base class, I can treat them all the same and will be able to save and load them without concern of their differences.
To help emphasize the differences between our monsters, Create three different prefabs in your project, one for each monster. You might make one with a sphere and one with a cube, but at a minimum color them all based on their name to help show that there is in fact a difference. Make sure and assign the specific sub class script to each type of monster and not the base class version of itself.
Now we need to modify the Demo script for our new functionality:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
public class Demo : MonoBehaviour
{
[SerializeField] GameObject[] prefabs;
Dictionary<string, GameObject> mapping = new Dictionary<string, GameObject>();
List<Monster> monsters = new List<Monster>();
void Start ()
{
for (int i = 0; i < prefabs.Length; ++i)
mapping.Add(prefabs[i].name, prefabs[i]);
Load ();
}
void OnGUI ()
{
if (GUI.Button(new Rect(10, 10, 100, 30), "Add"))
AddRandom ();
if (GUI.Button(new Rect(10, 50, 100, 30), "Save"))
Save ();
if (GUI.Button(new Rect(10, 90, 100, 30), "Load"))
Load ();
}
void Randomize (Monster monster)
{
monster.HP = UnityEngine.Random.Range(10, 100);
float x = UnityEngine.Random.Range(-10f, 10f);
float y = UnityEngine.Random.Range(-10f, 10f);
float z = UnityEngine.Random.Range(-10f, 10f);
monster.transform.localPosition = new Vector3(x, y, z);
}
void AddRandom ()
{
GameObject prefab = prefabs[ UnityEngine.Random.Range(0, prefabs.Length) ];
Monster monster = Add(prefab);
Randomize(monster);
}
Monster Add (GameObject prefab)
{
GameObject instance = Instantiate(prefab) as GameObject;
Monster monster = instance.GetComponent<Monster>();
monster.name = prefab.name;
monsters.Add(monster);
return monster;
}
void Save ()
{
var monsterData = new List<Dictionary<string, object>>( monsters.Count );
for (int i = 0; i < monsters.Count; ++i)
monsterData.Add( monsters[i].Save() );
string json = Json.Serialize(monsterData);
Debug.Log(json);
PlayerPrefs.SetString( "Monsters", json );
}
void Load ()
{
Clear();
string json = PlayerPrefs.GetString( "Monsters", string.Empty );
if (!string.IsNullOrEmpty(json))
{
var monsterData = Json.Deserialize(json) as List<object>;
for (int i = 0; i < monsterData.Count; ++i)
{
Dictionary<string, object> data = monsterData[i] as Dictionary<string, object>;
string prefab = (string)(data["Prefab"]);
Monster monster = Add ( mapping[prefab] );
monster.Load( data );
}
}
}
void Clear ()
{
for (int i = monsters.Count - 1; i >= 0; --i)
Destroy(monsters[i].gameObject);
monsters.Clear();
}
}
In this version, I no longer need the reference to the single Monster object. Since we are creating from a variety of monster types dynamically, I need references to the prefabs which we can Instantiate (line 8). These could have been obtained through Resources.Load, but this version is acceptable for now. Donât forget to assign them in the editor.
Next I created a dictionary to map from a prefab name to the prefab itself (line 9) – that setup occurs in the Start method (lines 14-15). This step may seem a bit redundant, but I find the code a bit more readable (not to mention more efficient) than code which needs to search the array for a match (lots of string comparisons = SLOW).
I only slightly modified the OnGUI code – I replaced the button which moved the original Monster around, with a button to create new dynamic monsters which will already be moved around.
The âRandomizeâ method now requires a Monster parameter to be passed to it, since we want to be able to move any of our monsters with the same code.
The âAddRandomâ method is new and picks one of the prefabs at random to create. It then makes sure the newly created monster is Randomized with the previously mentioned method.
The âAddâ method takes a prefab as a parameter, from which it will instantiate a new monster. The monster script stores a reference to the name of the prefab which was used, so that we can use the same prefab again later when we need to actually persist the object. After instantiating the monster, we get a reference to the monster script (note that the GetComponent will work because our subclass versions of Monster are still Monster components) and add the component to a list so we can easily keep track of and manage everything we have created.
The Save method is similar to the previous version, but now we are creating a list of dictionary objects, instead of just a single dictionary. Our JSON serializer can serialize the whole thing as one string, so we can easily stick it in a PlayerPref as we did before. Note that I used a pluralized key this time for clarity.
The Load method changed a bit more, but not too bad. First I destroy any existing monsters before creating new ones according to the Save data. Note that this is easier than a Queue system, but a queue of reusable objects is more ideal in production code. The other big difference (besides working with a list of dictionaries) is that I get a reference to the prefab name within each entry and Instantiate a new object accordingly. Then I load the corresponding data on the spawned object. This is important because a Red Monster would fail to load correctly if it were passed a Blue Monsterâs data and vice-versa.
Feel free to run the scene and give everything a try. Add a bunch of monsters, save the data (note that I also log a version of the JSON output to the console in case you want to inspect it) and then try stopping and starting a new session. All of your dynamically created monsters load back just fine!
Files
If you are saving lots of data, or if it makes sense to break your data up into smaller chunks which can be loaded at different times, then it may make sense to write to files instead of putting too much into PlayerPrefs.
Using the same example from before you can modify just the Save and Load methods as follows:
void Save ()
{
var monsterData = new List<Dictionary<string, object>>( monsters.Count );
for (int i = 0; i < monsters.Count; ++i)
monsterData.Add( monsters[i].Save() );
string json = Json.Serialize(monsterData);
string filePath = Application.persistentDataPath + "/Monsters.txt";
File.WriteAllText(filePath, json);
}
void Load ()
{
Clear();
string filePath = Application.persistentDataPath + "/Monsters.txt";
if (File.Exists(filePath))
{
string json = File.ReadAllText(filePath);
var monsterData = Json.Deserialize(json) as List<object>;
for (int i = 0; i < monsterData.Count; ++i)
{
Dictionary<string, object> data = monsterData[i] as Dictionary<string, object>;
string prefab = (string)(data["Prefab"]);
Monster monster = Add ( mapping[prefab] );
monster.Load( data );
}
}
}
Summary
In this lesson we covered a few methods of data persistence. First we covered temporary persistence through static classes, the Singleton design pattern, and Unity GameObjects which can survive scene changes. Then we explored how data can be persisted even across multiple play sessions via PlayerPrefs, JSON serialization and writing data to files. We even got a bit fancy by persisting a list of dynamically created polymorphic objects.