Okay. Finally got it working. Here’s what I did:
First made the data object. It stores all the relevant information to recreate an object. Apart from primary data like transform fields, name, ID and parentID, it also holds any additional data using dictionaries. Used exactly like PlayerPrefs, but for every single monobehaviour.
/// <summary>
/// Used to store data of LevelObject derived classes.
/// </summary>
[System.Serializable]
public class LevelObjectData
{
public string name;
public int id;
public int? parentId;
[JsonProperty("Vectors")]
Dictionary<string, float[]> vectors;
[JsonProperty("Ints")]
Dictionary<string, int> ints;
[JsonProperty("Floats")]
Dictionary<string, float> floats;
[JsonProperty("Bools")]
Dictionary<string, bool> bools;
[JsonProperty("Strings")]
Dictionary<string, string> strings;
public LevelObjectData(){}
public LevelObjectData(LevelObject levelObject)
{
vectors = new();
ints = new();
floats = new();
bools = new();
strings = new();
name = levelObject.name.Replace("(Clone)", "").Trim();
SetVector("position", levelObject.transform.position);
SetVector("eulerAngles", levelObject.transform.eulerAngles);
SetVector("scale", levelObject.transform.localScale);
id = levelObject.GetID();
parentId = levelObject.transform.parent?.GetComponent<LevelObject>()?.GetID();
}
public void SetInt(string key, int value)
{
ints[key] = value;
}
public void SetFloat(string key, float value)
{
floats[key] = value;
}
public void SetVector(string key, Vector3 value)
{
vectors[key] = new float[]{value.x, value.y, value.z};
}
public void SetBool(string key, bool value)
{
bools[key] = value;
}
public void SetString(string key, string value)
{
strings[key] = value;
}
public int GetInt(string key)
{
return ints[key];
}
public float GetFloat(string key)
{
return floats[key];
}
public Vector3 GetVector(string key)
{
return new Vector3(vectors[key][0], vectors[key][1], vectors[key][2]);
}
public bool GetBool(string key)
{
return bools[key];
}
public string GetString(string key)
{
return strings[key];
}
}
Then made a parent class that all my monobehaviours with relevant information will derive from, it’s called LevelObject. It essentially handles the serialization behaviour. It generates an ID for itself and fills the data object with essential information.
/// <summary>
/// Base class for handling serialization/deserialization of level objects.
/// </summary>
public class LevelObject : MonoBehaviour
{
public static Dictionary<int, LevelObject> idToObject = new();
public static string defaultPrefabPath = "Level Objects/";
LevelObjectData levelObjectData;
/// <summary>
/// Generates a level object ID from from name, position and rotation
/// </summary>
/// <returns>An object ID.</returns>
public int GetID()
{
return string.Format("{0}-{1}-{2}", name, transform.position, transform.eulerAngles).GetHashCode();
}
/// <summary>
/// Generates and returns data of the level Object.
/// </summary>
public LevelObjectData SaveData()
{
levelObjectData = new(this);
levelObjectData = SaveCustomData(levelObjectData);
return levelObjectData;
}
/// <summary>
/// Saves the additional custom fields if there are any.
/// Override this function to serialize other custom fields.
/// </summary>
public virtual LevelObjectData SaveCustomData(LevelObjectData levelObjectData)
{
return levelObjectData;
}
/// <summary>
/// Initializes GameObject using the given data.
/// </summary>
/// <param name="levelObjectData">The data object.</param>
public void LoadData(LevelObjectData levelObjectData)
{
if(levelObjectData.parentId != null) transform.parent = idToObject[(int)levelObjectData.parentId].transform;
name = levelObjectData.name;
transform.position = levelObjectData.GetVector("position");
transform.eulerAngles = levelObjectData.GetVector("eulerAngles");
transform.localScale = levelObjectData.GetVector("scale");
if (levelObjectData.id != GetID())
{
Debug.LogWarning("ID does not match with saved ID!");
}
idToObject[GetID()] = this;
this.levelObjectData = levelObjectData;
}
/// <summary>
/// Restores the additional custom fields if there are any. Runs after whole hierarchy is instantiated and assigned their ID's.
/// Override this function to deserialize other custom fields.
/// </summary>
/// <param name="levelObjectData">The data object.</param>
public virtual void LoadCustomData(LevelObjectData levelObjectData)
{
}
/// <summary>
/// Extracts all level object data from the hierarchy and converts it to json. Works recursively.
/// </summary>
public string HierarchyToJson()
{
List<LevelObjectData> levelObjects = new();
HierarchyToData(ref levelObjects);
print(levelObjects.Count);
return JsonConvert.SerializeObject(levelObjects, Formatting.Indented);
}
public void HierarchyToData(ref List<LevelObjectData> levelObjects)
{
levelObjects.Add(this.SaveData());
foreach(LevelObject levelObject in GetComponentsInChildren<LevelObject>())
{
if(levelObject != this)
{
levelObjects.Add(levelObject.SaveData());
}
}
}
/// <summary>
/// Instantiates and initalizes all level objects in the given data and reconstructs the hierarchy.
/// </summary>
/// <param name="data">Json string of a list of LevelObjectData.</param>
/// <returns>The root GameObject</returns>
public static GameObject JsonToHierarchy(string data)
{
List<LevelObjectData> levelObjectDatas = (List<LevelObjectData>)JsonConvert.DeserializeObject(data, typeof(List<LevelObjectData>));
List<LevelObject> levelObjects = new();
foreach (LevelObjectData levelObjectData in levelObjectDatas)
{
GameObject go = Resources.Load<GameObject>(defaultPrefabPath + levelObjectData.name);
LevelObject lo;
if (go != null)
{
lo = Instantiate(go).GetComponent<LevelObject>();
}
else
{
lo = new GameObject().AddComponent<LevelObject>();
}
lo.LoadData(levelObjectData);
levelObjects.Add(lo);
}
//second loop because all objects needed to be instantiated to enable referencing other objects.
foreach (LevelObject lo in levelObjects)
{
lo.LoadCustomData(lo.levelObjectData);
}
return levelObjects[0].gameObject;
}
private void OnDestroy()
{
idToObject.Remove(GetID());
}
}
Two of the functions can be overriden to save/load additional data like so:
public override void LoadCustomData(LevelObjectData levelObjectData)
{
target = idToObject[levelObjectData.GetInt("targetID")].GetComponent<BlockLaser>();
}
public override LevelObjectData SaveCustomData(LevelObjectData levelObjectData)
{
levelObjectData.SetInt("targetID", target.GetID());
return levelObjectData;
}
Some notes:
- For finding it’s own prefab reference, it only uses it’s GameObject name. All prefabs that you want serialized must be in the resources folder and must retain it’s name in the scene (the code automatically removes “(Clone)” tag).
- Whole object hierarchies can be serialized with HierarchyToJson() function but every object that you want to be saved must have a component derived from LevelObject