# How to Serialize Content Placed at Runtime for Persistence Source: https://www.nianticspatial.com/docs/nsdk/how-to/ar/serialize_runtime_scenes/ This how-to demonstrates: - Registering prefabs for spawning at runtime - Storing transform and stateful data for restoring content in a future session - Updating previously serialized data > **Note:** > > This how-to uses manually constructed verbose strings to demonstrate serialization in a human-readable format. For proper serialization, we recommend using `[Serializable]` structs with a JSON serializer for a concrete structure or a binary serializer for compact storage. ## Prerequisites You will need a Unity project with NSDK AR enabled. For more information, see [Set up the Niantic SDK for Unity](https://www.nianticspatial.com/docs/nsdk/setup/#set-up-the-niantic-sdk-for-unity). ## Basic Concepts for Serialization of Runtime Content As opposed to [Remote Content Authoring](https://www.nianticspatial.com/docs/nsdk/how-to/vps/real_world_location_ar/), where Unity scenes contain static placed objects in each `ARLocation`, runtime content serialization allows players to place content arbitrarily, then serialize the placed content into a known format so that it can be re-spawned in the future. To serialize a player-constructed scene, this information is needed: 1. The root transform that anchors the content (ARLocation, Image Target, etc); - This ensures that future deserialized content will be placed in the same location. 2. All `GameObjects` that the player has placed; 3. The type and state (transform and color, in this example) of each of these `GameObjects`. This data is then compacted into a string or binary blob for retrieval the next time a player tracks that same `ARLocation`. To deserialize the data and reconstruct the scene: 1. Retrieve and instantiate each `GameObject` from the serialized data; - Each `GameObject` is placed under the same root transform (`ARLocation`) so that it appears in the same place. 2. Apply the stateful data (transform, color, etc.) of each `GameObject`. A sample scene, "VpsSceneSerialization", is provided to follow along with the scripts in this How-To. ## Registering Prefabs for Serialization The first problem to solve is gathering a list of all possible Prefabs that players can place. This is necessary to reconstruct the scene in the future. The script `PersistentObject.cs` allows for finding spawnable Prefabs with a `GetComponent` and identifying each unique Prefab with a `PrefabId`. #### Click to expand the PersistentObject class ```cs using UnityEngine; public class PersistentObject : MonoBehaviour { [SerializeField] private long _prefabId; public long PrefabId { get { return _prefabId; } internal set { _prefabId = value; } } ... // Serialization code } ``` In the "VpsSceneSerialization" sample, there are two `PersistentObject` Prefabs, a cube and a sphere, assigned the `PrefabIds` 0 and 1 respectively. These `PersistentObjects` are then registered to a `PersistentObjectManager` to create a complete list of spawnable objects. (image: Persistent Object Manager component) #### Click to expand the PersistentObjectManager class ```cs using System.Collections.Generic; using System.Collections.ObjectModel; public class PersistentObjectManager : MonoBehaviour { [SerializeField] private List _persistentObjects = new List(); public ReadOnlyCollection PersistentObjects => _persistentObjects.AsReadOnly(); ... // Serialization and validation code } ``` ## Runtime Content Placement The "VpsSceneSerialization" sample has two scripts; `PersistentContentDemoManager.cs` and `PersistentObjectPlacementBehaviour.cs`. These scripts manage spawning/destroying `Cubes` and `Spheres` whenever the player taps a plane or existing `GameObject`. Each game will have its own rules for spawning and destroying objects, so this How-To will not go into the details of making those scripts. > **Caution:** > > **Attention** > > The following line of code implements an important detail - all spawned objects must be children of the `ARLocation` after instantiation! Make sure to do this when building your own application! ```cs // _root is a reference to the ARLocation that is currently tracked spawned.transform.SetParent(_root.transform, true); ``` ## Serializing and Storing Spawned Objects After the player has built their scene and is ready to serialize it for storage, we must create a list of all spawned `GameObjects`. There are several ways to do this, such as holding each `GameObject` in a `List`, or, as this sample does, using a `GetComponent` on the root `GameObject`. #### Click to expand the updated PersistentObjectManager script ```cs public class PersistentObjectManager : MonoBehaviour { [SerializeField] private List _persistentObjects = new List(); private const char Delimiter = ';'; private readonly Dictionary _validObjects = new Dictionary(); public string SerializeObjectsToString(GameObject rootGo = null) { var root = rootGo ? rootGo.transform : this.transform; var serializedObjects = new List(); ValidateAndRefreshPersistentObjectList(); foreach (var child in root) { var persistentObject = ((Transform)child).GetComponent(); if (persistentObject) { if (!_validObjects.Keys.ToList().Contains(persistentObject.PrefabId)) { continue; } serializedObjects.Add(persistentObject.SerializeObjectToString()); } } return string.Join(Delimiter, serializedObjects.ToArray()); } ... // Deserialization and validation code } ``` For simplicity, this sample assumes that all Prefabs are children of the same root with no nested Prefabs. A real application may require better tracking and handling of Prefabs in the Hierarchy. After gathering the `List` of `PersistentObjects`, each `PersistentObject` handles its own serialization into a string. These strings are then joined into a single string representation for storage. #### Click to expand the updated PersistentObject script ```cs public class PersistentObject : MonoBehaviour { [SerializeField] private long _prefabId; public long PrefabId { get { return _prefabId; } internal set { _prefabId = value; } } public virtual string SerializeObjectToString() { return SimpleObjectToString(); } protected string SimpleObjectToString() { var stringFormat = $"PrefabId: {_prefabId}, Position: {SerializeVector3(transform.localPosition)}, " + $"Rotation: {SerializeQuaternion(transform.localRotation)}, Scale: {SerializeVector3(transform.localScale)}"; return stringFormat; } private string SerializeVector3(Vector3 vector) { return string.Format("{0},{1},{2}", vector.x, vector.y, vector.z); } private string SerializeQuaternion(Quaternion quat) { return string.Format("{0},{1},{2},{3}", quat.x, quat.y, quat.z, quat.w); } ... // Deserialization code } ``` Serialization returns a string that contains the relevant information for reconstructing the scene in a future session. In this example, the string looks like this: `PrefabId: 0, Position: 0,0,0, Rotation: 0,-0.2323422,0,0.9726341, Scale: 1,1,1; PrefabId: 1, Position: 0,0,-1.689, Rotation: 0,0,0,1, Scale: 1,1,1` The serialized string can then be stored in a backend or service for future retrieval. In this sample, the string is stored locally in `PlayerPrefs`, indexed by the name of the `ARLocation` Anchor. ```cs // Serialize the content and store it var serializedString = PersistentObjectManager.SerializeObjectsToString(_root); // _locationString is just persistentAnchor.name PlayerPrefs.SetString(_locationString, serializedString); ``` ## Extending PersistentObject to Store More State In the "VpsSceneSerialization" sample, the Cube and Sphere are `ColorfulPersistentObjects` rather than base `PersistentObjects`. We extend the base class here to show serializing more custom stateful data on top of the basic transform. #### Click to expand the ColorfulPersistentObject class ```cs using UnityEngine; public class ColorfulPersistentObject : PersistentObject { public override string SerializeObjectToString() { Color color; if (!Application.isPlaying) { // Have to use sharedMaterial in edit mode to not leak materials, // this means all gameobjects that share a material (default) will have the same color color = GetComponent().sharedMaterial.color; } else { color = GetComponent().material.color; } var colorStr = SerializeColor(color); return $"{base.SerializeObjectToString()}, Color: {colorStr}"; } private string SerializeColor(Color color) { return $"{color.r},{color.g},{color.b},{color.a}"; } ... // Deserialization code } ``` This extension adds the `Color` field to each serialized `GameObject` for future restoration. Our sample serialized data string now looks like this: `PrefabId: 0, Position: 0,0,0, Rotation: 0,-0.2323422,0,0.9726341, Scale: 1,1,1, Color: 1,1,1,1;PrefabId: 1, Position: 0,0,-1.689, Rotation: 0,0,0,1, Scale: 1,1,1, Color: 1,1,1,1` ## Restoring Serialized Content (Deserialization) The serialized data string is then restored in a future session by `PersistentObjectManager.cs`. #### Click to expand the updated PersistentObjectManager script ```cs using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text.RegularExpressions; using UnityEngine; public class PersistentObjectManager : MonoBehaviour { [SerializeField] private List _persistentObjects = new List(); public ReadOnlyCollection PersistentObjects => _persistentObjects.AsReadOnly(); private const char Delimiter = ';'; private const string PrefabIdReg = "PrefabId: (?\\d*)"; private Regex _prefabIdRegex = new Regex(PrefabIdReg); private readonly Dictionary _validObjects = new Dictionary(); ... // Serialization and validation code public List DeserializeObjectsFromString(string str, GameObject rootGo = null) { var spawnedObjects = new List(); var root = rootGo ? rootGo.transform : this.transform; ValidateAndRefreshPersistentObjectList(); var objs = str.Split(Delimiter); foreach (var serializedObj in objs) { var match = _prefabIdRegex.Match(serializedObj); if (!match.Success) { Debug.LogError("Could not parse out prefab id"); continue; } var prefabId = long.Parse(match.Groups["pid"].Value); if (!_validObjects.ContainsKey(prefabId)) { Debug.LogError($"Could not find prefab id {prefabId}"); continue; } var persistentObject = _validObjects[prefabId]; var newObject = Instantiate(persistentObject.gameObject, root); newObject.GetComponent().ApplyStringToObject(serializedObj); spawnedObjects.Add(newObject); } return spawnedObjects; } } ``` The `DeserializeObjectsFromString` method takes a serialized string, splits it into individual `GameObjects`, then attempts to spawn registered `PersistentObjects` according to the recorded `PrefabIds`. Each `GameObject` string is then passed to the `PersistentObject` (or, in our example, `ColorfulPersistentObject`) for each `GameObject` to handle individually. > **Caution:** > > Make sure the list and contents of the `_persistentObjects` List stay consistent between serialization and deserialization! Otherwise, nonsense objects may spawn instead. When making a full game, we recommend using a versioning scheme to ensure that the List stays consistent between operations. #### Click to reveal the updated ColorfulPersistentObject deserialization script ```cs using System.Text.RegularExpressions; using UnityEngine; public class ColorfulPersistentObject : PersistentObject { private const string ColourReg = "Color: (?.*,.*,.*,.*)"; private static Regex _colourRegex = new Regex(ColourReg); ... // Serialization code public override void ApplyStringToObject(string str) { base.ApplyStringToObject(str); var match = _colourRegex.Match(str); if (!match.Success) { Debug.Log("No match"); return; } var colorString = match.Groups["color"].Value; var color = DeserializeColor(colorString); ApplyColorToObject(color); } public void ApplyColorToObject(Color color) { Material mat; if (!Application.isPlaying) { // Have to use sharedMaterial in edit mode to not leak materials mat = GetComponent().sharedMaterial; } else { mat = GetComponent().material; } mat.color = color; } private Color DeserializeColor(string str) { // Deserialize a color from a string var split = str.Split(','); return new Color(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2]), float.Parse(split[3])); } } ``` The extension `ColorfulPersistentObject` handles the Color portion of the serialized string, then hands the rest to the base `PersistentObject` class to handle the transform data. This is inefficient, so let's clean it up: #### Click to reveal the optimized PersistentObject script ```cs using System.Text.RegularExpressions; using UnityEngine; public class PersistentObject : MonoBehaviour { [SerializeField] private long _prefabId; public long PrefabId { get { return _prefabId; } internal set { _prefabId = value; } } private static string regStr = "PrefabId: (?\\d*), Position: (?.*,.*,.*), Rotation: (?.*,.*,.*,.*), Scale: (?.*,.*,.*)"; private static Regex regex = new Regex(regStr); public virtual void ApplyStringToObject(string str) { // Parse the string and apply it to the object SimpleApplyStringToObject(str); } protected void SimpleApplyStringToObject(string str) { var match = regex.Match(str); if (!match.Success) { Debug.Log("No match"); return; } var prefabId = match.Groups["pid"].Value; var position = match.Groups["pos"].Value; var rotation = match.Groups["rot"].Value; var scale = match.Groups["scale"].Value; _prefabId = long.Parse(prefabId); transform.localPosition = DeserializeVector3(position); transform.localRotation = DeserializeQuaternion(rotation); transform.localScale = DeserializeVector3(scale); } private Vector3 DeserializeVector3(string str) { var split = str.Split(','); return new Vector3(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2])); } private Quaternion DeserializeQuaternion(string str) { var split = str.Split(','); return new Quaternion(float.Parse(split[0]), float.Parse(split[1]), float.Parse(split[2]), float.Parse(split[3])); } } ``` ## Updating Serialized Data To update serialized data, restore the data, allow players to modify the scene (delete `GameObjects`, move them, etc.), and then reserialize and overwrite the previously stored data. (For very large scenes, constantly reading and rewriting the entire scene is inefficient, so we recommend an incremental update system.) ## More Information For more information on using Location AR, see [Creating Location AR Experiences with VPS](https://www.nianticspatial.com/docs/nsdk/features/lightship_vps/).