A simple save system in Unity

Saving game data is a task that I need regularly. Using serialization and writing data to a file is an approach that I have used much, but that does not work in WebGL builds. Instead, I wrote data in a PlayerPrefs string. In this way I do not need to keep a details about what objects have a save data to delete or find all save data. The XML string stored contains everything.

All objects that should be saved needs to inherit PersistentObject interface and implement a function PersistentObject.Save(). Save should use ObjectManager.SaveInt(), SaveString() or SaveVector() functions to add data to be saved. In Start() function, one reads data and call ObjectManager.RegisterPersistentObject(this) and read the save data; to notify ObjectManager that the class instance wants to be saved. Then save points needs to call ObjectManager.SavePersistent() to save data to PlayerPrefs string. See an example at the end of the post.

Public functions

  • SaveExists(): returns true if there the game has save data.
  • SaveInt(string k, int d): adds an int d to be saved, k is the key to get the data (k needs to be unique).
  • SaveString(string k, string d)
  • SaveVector3(string k, Vectro3 d)
  • string GetString(string k, string def=””): returns string with key k or def value if there is no value matching the key.
  • int GetInt(string k, int def=0)
  • Vector3 GetVector3(string k): returns Vector3 with key k. NegativeInfinity is returned if there is no data matching the key.
  • PersistentObject.Save(): saves data. Call this in your save points script.
 using UnityEngine;
 using System.Collections;
 using System.Collections.Generic;
 using System.Text;
 using System.Xml.Serialization;
 using System;
 using System.IO;
 using UnityEngine.UI;
 
 public interface PersistentObject {
     void Save();
 }
 
 [Serializable()]
 public class DataItem: IEquatable<DataItem> {
     public string key;
     public string data;
 
     public override bool Equals(object obj)
     {
         if (obj == null) return false;
         DataItem objAsDataItem = obj as DataItem;
         if (objAsDataItem == null) return false;
         else return Equals(objAsDataItem);
         }
         public override int GetHashCode()
         {
         return key.GetHashCode();
         }
 
     public bool Equals(DataItem other)
     {
                if (other == null) return false;
             return (this.key.Equals(other.key));
     }
 }
 
 public class ObjectManager : MonoBehaviour {
     private ArrayList persistentObjects;
     private static ObjectManager instance = null;
     private List<DataItem> persistentData = null;
     private const string SAVE_KEY = "FO";
 
     void Awake () {
         if(instance != null) {
             Destroy(gameObject);
             return;
         }
         instance = this;
         persistentData = new List<DataItem>();
         persistentObjects = new ArrayList();
         if(SaveExists()) {
             persistentData =  Deserialize<List<DataItem>>(PlayerPrefs.GetString(SAVE_KEY));    
         }
     }
 
     public static bool SaveExists() {
         return PlayerPrefs.HasKey(SAVE_KEY);
     }
 
     private static T Deserialize<T>(string xml) {
           var xs = new XmlSerializer(typeof(T));
           return (T)xs.Deserialize(new StringReader(xml));
     }
 
     private void SaveData(string k, string d) {
         DataItem di = new DataItem { key = k, data = d };
         if(persistentData.Contains(di)) {
             DataItem upd = persistentData.Find(x => x.key == k);
             upd.data = d;
         }
         else {
             persistentData.Add(di);    
         }
         string saveData = DataToString();
         PlayerPrefs.SetString(SAVE_KEY, saveData);
     }
 
     public static void SaveInt(string k, int d) {
         instance.SaveData(k, d.ToString());
     }
 
     public static void SaveVector3(string k, Vector3 d) {
         instance.SaveData(k + "_x", d.x.ToString());
         instance.SaveData(k + "_y", d.y.ToString());
         instance.SaveData(k + "_z", d.z.ToString());
     }
 
     public static void SaveString(string k, string d) {
         instance.SaveData(k, d);
     }
 
     private DataItem GetData(string k) {
         return instance.persistentData.Find(x => x.key == k);
     }
 
     public static string GetString(string k, string def="") {
         DataItem di = instance.GetData(k);
         if(di == null) {
             return def;
         }
         else {
             return di.data;
         }
     }
 
     public static int GetInt(string k, int def=0) {
         DataItem di = instance.GetData(k);
         if(di == null) {
             return def;
         }
         else {
             return Int32.Parse(di.data);
         }
     }
 
     public static Vector3 GetVector3(string k) {
         DataItem di_x = instance.GetData(k + "_x");
         DataItem di_y = instance.GetData(k + "_y");
         DataItem di_z = instance.GetData(k + "_z");
         if(di_x == null) {
             return Vector3.negativeInfinity;
         }
         else {
             
             float x = float.Parse(di_x.data);
             float y = float.Parse(di_y.data);
             float z = float.Parse(di_z.data);
             return new Vector3(x, y, z);
         }
     }
 
     private string DataToString() {
         var xs = new XmlSerializer(typeof(List<DataItem>));    
         var xml = new StringWriter();
         xs.Serialize(xml, persistentData);
         string d = xml.ToString();
         return d;
     }
 
     public static void RegisterPersistentObject(PersistentObject o) {
         instance.persistentObjects.Add(o);
     }
 
     public static void DeleteSaveData() {
         PlayerPrefs.DeleteKey(SAVE_KEY);
     }
 
     public static void SavePersistent() {
         Debug.Log("ObjectManager.SavePersistentData()");
         foreach(PersistentObject o in instance.persistentObjects) {
             o.Save();
         }
         PlayerPrefs.Save();
     }
 }
 

Just attach the ObjectManager script to a game object (a scene should have one ObjectManager).

For example, if one wants to save position and rotation of the object following code will do that for the object it is attached to:

 public class SavePositionRotation : MonoBehaviour, PersistentObject {
     void Start () {
         ObjectManager.RegisterPersistentObject(this);
         Vector3 p = ObjectManager.GetVector3(name + "_p");
         Vector3 r = ObjectManager.GetVector3(name + "_r");
 
         if (!float.IsInfinity(p.x)) {
             transform.position = p;
             transform.eulerAngles = r;
         }
         // else use the original position of the gameObject
     }
     void PersistentObject.Save()
     {
         ObjectManager.SaveVector3(name + "_p", transform.position);
         ObjectManager.SaveVector3(name + "_r", transform.eulerAngles);
     }
 }

Note that the name of the gameObject is used as the main part of the save key here. An implication of this is that all objects saved needs to have an unique name.

 

Published by lankoski

Petri Lankoski, D.Arts, is a Associate Professor in Game Studies at the school of Communication, Media and IT at the Södertörn University, Sweden. His research focuses on game design, game characters, role-playing, and playing experience. Petri has been concentrating on single-player video games but researched also (multi-player) pnp and live-action role-playing games. This blog focuses on his research on games and related things.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: