Serialize and save a list of Vector3's and Quaternions

Hey People
I am currently working on a program in Unity that simulates the path of a robot given some Vector3 and Quaternions given from a Json file so far I have managed to create a method for serializing the gameObject in my unity scene, now I want to be able to save and load a List of Vector3’s and Quaternions for the GameObject to move/slerp towards, for testing purposes I have hardcoded some vector3’s and Quaternions the code for that is as follows (bottom of the code snippet):

public class TestSimulation : MonoBehaviour
{
    // Field for the Text object. Meant to represent the x, y, z position of the GameObject.
    // Serialized in order to make it easy to set in the editor.
    // Drag a TextMeshPro text object to this field in the inspector
    [SerializeField]
    private TMPro.TextMeshProUGUI positionText;
    // Field for the Text object. Meant to represent the x, y, z, w, rotation of the GameObject.
    [SerializeField]
    private TMPro.TextMeshProUGUI rotationText;


    // Index used to navigate through the lists:
    private int waypointIndex;
    // Create list of vector3 positions
    private List<Vector3> pointsPosition = new List<Vector3>();
    // Create list of Quaternion rotations;
    private List<Quaternion> pointsRotation = new List<Quaternion>();

    // Get the target Vector3.
    private Vector3 targetPosition;
    // Field for the targetRotation/target Quarternion
    private Quaternion targetRotation;

    // Field for movementSpeed variable to increase or decrease the movement speed of the GameObject.
    // If the movementSpeed is 0 then then GameObject will not move,
    // and if the movementSpeed is negative then the GameObject will never reach it's target position.
    // Therefore recommended that the movementSpeed is kept positive.
    [SerializeField]
    private float movementSpeed;
    // Field for rotationSpeed variable to increase or decrease the rotation speed of the GameObject.
    [SerializeField]
    private float rotationSpeed;

    // Start is called before the first frame update
    void Start()
    {
        // Call to populateLists method at start to populate the ppintsPosition and pointsRotation lists.
        populateLists();
        // Make sure the waypointIndex is set to 0 at start
        waypointIndex = 0;

        // Set the start vector3 position equal item 0 in pointsPosition list.
        targetPosition = pointsPosition[0];
        // Set the Start Quaternion rotation equal item 0 in pointsRotation list.
        targetRotation = pointsRotation[0];
    }

    // Update is called once per frame
    void Update()
    {
        // Display the current position of the GameObject. The text to appear in in the Text object.
        positionText.text = string.Format("Position:\n{0}", transform.position);
        // Display the current euler rotation of the GameObject. The text to appear in the Text object.
        rotationText.text = string.Format("Rotation:\n{0}", transform.rotation.eulerAngles);

        // Move towards the targetPosition.
        transform.position = Vector3.MoveTowards(transform.position, targetPosition, movementSpeed * Time.deltaTime);
        // Rotate towards the target rotation.
        transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationSpeed * Time.deltaTime);

        // Check if the GameObject is within a certain distance of the targetPosition
        if (Vector3.Distance(transform.position, targetPosition) < .01f)
        {
            new WaitForSeconds(1);
            // Check to make sure that the waypointIndex is NOT bigger than than the maximum number of vector3's in pointsPosition.
            if (waypointIndex >= pointsPosition.Count - 1)
            {
                // If the waypointIndex is bigger than or equal to the total number of Vector3's in pointsPosition then reset the waypointIndex to 0.
                waypointIndex = 0;
                // If the waypointIndex is bigger than or equal to the total number of Vector3's in pointsPosition then pause the game.
                Time.timeScale = 0;
            }
            else
            {
                waypointIndex++;
            }
            // Set a new target position
            targetPosition = pointsPosition[waypointIndex];
            // Set a new target rotation
            targetRotation = pointsRotation[waypointIndex];
        }
    }

    /// <summary>
    /// Populate the pointsPosition list with Vector3's and the pointsRotation with Quaternion's
    /// </summary>
    void populateLists()
    {
        Vector3 rotationsVector;

        pointsPosition.Add(new Vector3(0, 1, 0));
        pointsPosition.Add(new Vector3(1, 1, 0));
        pointsPosition.Add(new Vector3(1, 1, 1));
        pointsPosition.Add(new Vector3(2, 2, 2));
        pointsPosition.Add(new Vector3(3, 3, 3));
        pointsRotation.Add(Quaternion.Euler(0, 0, 0));
        pointsRotation.Add(Quaternion.Euler(45, 0, 0));
        pointsRotation.Add(Quaternion.Euler(0, 0, 0));
        // When NOT using Quaternion.Euler remember to convert input to float and add a w with value=1 as seen below.
        pointsRotation.Add(new Quaternion((float)0.45, 0, 0, 1));
        rotationsVector = new Vector3(25, 0, 0);
        pointsRotation.Add(Quaternion.Euler(rotationsVector));
    }
}

The data I am currently saving to Json (using LitJson) is the spawn/start position of the GameObject and the scene to load the data into.

to summarise:
I want to save the 2 lists (List pointsPosition and List pointsRotation) to a Json file, thus i need help serializing the Vector3’s and the Quaternions as well as deserializing Vector3’s and Quaternions from said file

In Unity serializer can not have arrays as root objects in json so you probably need to have something like this

[Serializable]
public class RobotPositionRecords {
  public List<Vector3> positions;
  public List<Quaternion> rotations;
}

It will work with unity built in serializer Unity - Manual: JSON Serialization

so the two lists I already have (pointsPosition (line 16) and pointsRotation (line 18)) is not enough lists, do I really need 2 new lists? or am I misunderstanding something?

No, that lists are good. But you need to enclose them into class to serialize/deserialize with unity serializer.

gotcha, do I then add the Json build code to said class or can I just reference the class in my saver class?

My points class

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

[Serializable]
public class Points : MonoBehaviour
{
    public List<Vector3> pointsPosition;
    public List<Quaternion> pointsRotation;
}

My SavingService.cs script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LitJson;
using System.IO;
using System.Linq;
using UnityEngine.SceneManagement;

/// <summary>
/// Any MonoBehaviour that implements the ISaveable interface will be saved in the scene, and loaded back.
/// </summary>
public interface ISaveable
{
    // The Save ID is a unique string that identifies a component in the save data.
    // It's used for finding that object again when the game is loaded.
    string SaveID { get; }

    // The SavedData is the content that will be written to disk.
    // It's asked for when the game is saved.
    JsonData SavedData { get; }

    // LoadFromData is called when the game is being loaded.
    // The object is provided with the data that was read,
    // and is expected to use that information to restore its previous state.
    void LoadFromData(JsonData data);
}

public static class SavingService
{
    // To avoid problems caused by typos,
    // we'll store the names of the strings we use to store and look up items in the JSON as constant strings.
    private const string ACTIVE_SCENE_KEY = "activeScene";
    private const string SCENES_KEY = "scenes";
    private const string OBJECTS_KEY = "objects";
    // (Use an unexpected character "$" for the Save ID here to reduce the chance of collisions).
    private const string SAVEID_KEY = "$saveID";

    private const string POINTS_STRING_KEY = "points";

    // A reference to the delegate that runs after the scene loads,
    // which performs the object state restoration.
    static UnityEngine.Events.UnityAction<Scene, LoadSceneMode>
        LoadObjectsAfterSceneLoad;

    ///// <summary>
    ///// Saves the game, and writes it to a file called fileName in the app's persistent data directory
    ///// </summary>
    public static void SaveGame(string fileName)
    {
        // Create the JsonData that we will eventually write to the disk
        var result = new JsonData();

        // Find All MonoBehaviours by first finding every MonoBehaviou,
        // and filtering it to only include those that are ISaveable.
        var allSaveableObjects = Object
            .FindObjectsOfType<MonoBehaviour>()
            .OfType<ISaveable>();

        // Check to see if we have any objects to save
        if (allSaveableObjects.Count() > 0)
        {
            // Create the JsonData that will store the list of objects.
            var savedObjects = new JsonData();

            // Iterate over every object we want to save
            foreach (var saveableObject in allSaveableObjects)
            {
                // Get the object's saved data
                var data = saveableObject.SavedData;

                // We expect this to be an object
                // (JSON's term for a dictionary)
                // because we need to include the object's Save ID
                if (data.IsObject)
                {
                    // Record the Save ID for this object
                    data[SAVEID_KEY] = saveableObject.SaveID;

                    // Add the object's save data to the collection
                    savedObjects.Add(data);
                }
                else
                {
                    // Provide a helpful warning that we can't save this object
                    var behaviour = saveableObject as MonoBehaviour;

                    Debug.LogWarningFormat(behaviour, "{0}'s save data is not a dictionary. The object was therefore not saved", behaviour.name);
                }
            }

            // Store the collection of saved objects in the result
            result[OBJECTS_KEY] = savedObjects;
        }
        else
        {
            // We have no objects to save. Give a nice warning.
            Debug.LogWarningFormat("The scene did not include any saveable objects.");
        }

        // Next, we need to record what scenes are open.
        // Unity lets you have multiple scenes open at the same time,
        // so we need to store all of them, as well as which scene is the "active" scene
        // (the scene that new objects are added to, and which controls the lighting settings for the game).

        // Create a JsonData that will store the list of open scenes,
        var openScenes = new JsonData();

        // Ask the scene manager how many scenes are open, and for each one store the scene's name.
        var sceneCount = SceneManager.sceneCount;

        for (int i = 0; i < sceneCount; i++)
        {
            var scene = SceneManager.GetSceneAt(i);
            openScenes.Add(scene.name);
        }

        // Store the list of open scenes
        result[SCENES_KEY] = openScenes;

        // Store the name of the active scene
        result[ACTIVE_SCENE_KEY] = SceneManager.GetActiveScene().name;

        // We've now finished generating the save data, and it's now time to write it to the disk.

        // Figure out where to put the file by combining the persistent data path with the filename,
        // that this method recieved as a parameter.
        var outputPath = Path.Combine(Application.persistentDataPath, fileName);

        // Create a JsonWriter, and configure it to 'pretty-print' the data.
        // This is optional (you could just call result.ToJson() with no JsonWriter parameter and receive a string),
        // but this way the resulting JSON is easier to read and understand, which is helpful while developing.
        var writer = new JsonWriter();
        writer.PrettyPrint = true;

        // Convert the save data to JSON text
        result.ToJson(writer);

        // Write the JSON text to the disk
        File.WriteAllText(outputPath, writer.ToString());

        // Notify where to find the save game file.
        Debug.LogFormat("Wrote saved game to {0}", outputPath);

        // We allocated a lot of memory here,
        // Which means that there's an increased chance of the garbage collector needing to run in the future.
        // To tidy up, we'll release our reference to the saved data,
        // and then ask the garbage collector to run immdiately.
        // This will result in a slight performance hitch as the collector runs,
        // but that's fine for this case, since users expect saving the game to pause for a second.
        result = null;
        System.GC.Collect();
    }

    /// <summary>
    /// Loads the game from a given file, and restores its state.
    /// </summary>
    public static bool LoadGame(string fileName)
    {
        // Figure out where to find the file.
        var dataPath = Path.Combine(Application.persistentDataPath, fileName);

        // Ensure that a file actually exists there.
        if (File.Exists(dataPath) == false)
        {
            Debug.LogErrorFormat("No file exists at {0}", dataPath);
            return false;
        }

        // Read the data as JSON.
        var text = File.ReadAllText(dataPath);
        var data = JsonMapper.ToObject(text);

        // Ensure that we successfully read the data, and that it's an object (i.e., a JSON dictionary)
        if (data == null || data.IsObject == false)
        {
            Debug.LogErrorFormat("Data at {0} is not a JSON object", dataPath);
            return false;
        }

        // We need to know what scenes to load.
        if (!data.ContainsKey("scenes"))
        {
            Debug.LogWarningFormat("Data at {0} does not contain any scenes; therefore not loading any", dataPath);
            return false;
        }

        // Get the list of scenes
        var scenes = data[SCENES_KEY];
        int sceneCount = scenes.Count;

        if (sceneCount == 0)
        {
            Debug.LogWarningFormat("Data at {0} doesn't speciy any scenes to load.", dataPath);
            return false;
        }

        // Load each specified scene.
        for (int i = 0; i < sceneCount; i++)
        {
            var scene = (string)scenes[i];

            // If this is the first scene we're loading, load it and replace every other active scene.
            if (i == 0)
            {
                SceneManager.LoadScene(scene, LoadSceneMode.Single);
            }
            else
            {
                // Otherwise, load that scene on top of the existing ones.
                SceneManager.LoadScene(scene, LoadSceneMode.Additive);
            }
        }

        // Find the active scene, and set it
        if (data.ContainsKey(ACTIVE_SCENE_KEY))
        {
            var activeSceneName = (string)data[ACTIVE_SCENE_KEY];
            var activeScene = SceneManager.GetSceneByName(activeSceneName);

            if (activeScene.IsValid() == false)
            {
                Debug.LogErrorFormat("Data at {0} specifies an active scene that doesn't exist. Stopping loading here.", dataPath);
                return false;
            }

            SceneManager.SetActiveScene(activeScene);
        }
        else
        {
            // This is not an error, since the first scene in the list will be treated as active,
            // but it's worth warning about.
            Debug.LogWarningFormat("Data at {0} does not specify an active scene.", dataPath);
        }

        // Find all objects in the scene and load them.
        if (data.ContainsKey(OBJECTS_KEY))
        {
            var objects = data[OBJECTS_KEY];

            // We can't update the state of the objects right away because Unity will not complete the scene
            // until some time in the future.
            // Changes we made to the objects would revert to how they're definde in the original scene.
            // As a result, we need to run the code after the scene manager reports that a scene has finished loading.

            // To do this, we create a new delegate that contains our object-loading code,
            // and store that in LoadObjectsAfterSceneLoad.
            // This delegate is added to the SceneManager's sceneLoaded event,
            // which makes it run after the scene finished loading.

            LoadObjectsAfterSceneLoad = (scene, LoadSceneMode) =>
            {
                // Find all ISaveable objects, and build a dictionary that maps their Save ID's to the object
                // (so that we can qickly look them up)
                var allLoadableObjects = Object
                    .FindObjectsOfType<MonoBehaviour>()
                    .OfType<ISaveable>()
                    .ToDictionary(o => o.SaveID, o => o);

                // Get the collection of objects we need to load
                var objectsCount = objects.Count;

                // For each item in the list...
                for (int i = 0; i < objectsCount; i++)
                {
                    // Get the saved data
                    var objectData = objects[i];

                    // Get the Save ID from that data
                    var saveID = (string)objectData[SAVEID_KEY];

                    // Attempt to find the object in the scene(s) that has that Save ID
                    if (allLoadableObjects.ContainsKey(saveID))
                    {
                        var loadableObject = allLoadableObjects[saveID];

                        // Ask the object to load from this data.
                        loadableObject.LoadFromData(objectData);
                    }
                }

                // Tidy up after ourselves; remove this delegate from the sceneLoaded event so that it isn't called next time
                SceneManager.sceneLoaded -= LoadObjectsAfterSceneLoad;

                // Release the reference to the delegate
                LoadObjectsAfterSceneLoad = null;

                // And ask the garbage collector to tidy up
                // (again, this will cause a performance hitch,
                // but users are fine with this as they're already waiting for the scene to finish loading)
                System.GC.Collect();
            };
            // Register the object-loading code t run ater the scene loads.
            SceneManager.sceneLoaded += LoadObjectsAfterSceneLoad;
        }
        return true;
    }
}

Again thank you for your help, because I must admit im drawing a complete blank with how to easy store/format the list of Vector3 and Quaternion to and from a Json file.

So I kinda solved my problem, granted im using a workaround that may not be the best solution available however the damned s**t works.

Now what I did was formatting my Vector3’s and Quaternion’s from their respective lists (se line 16 and line 18 in TestSimulation script in my initial post), into strings using the following method:

    public void buildStringList()
    {
        // Run through the list of points.
        for (int i = 0; i < pointsPosition.Count; i++)
        {
            // Separate the different values in each Vector3 in pointsPosition and Quaternion in pointsRotation
            string xp = pointsPosition[i].x.ToString();
            string yp = pointsPosition[i].y.ToString();
            string zp = pointsPosition[i].z.ToString();
            string xr = pointsRotation[i].x.ToString();
            string yr = pointsRotation[i].y.ToString();
            string zr = pointsRotation[i].z.ToString();
            string wr = pointsRotation[i].w.ToString();

            // Create and format a string and add it to the points list.
            stringsToSave.Add(string.Format("{0},{1},{2},{3},{4},{5},{6}", xp.Replace(",", "."), yp.Replace(",", "."), zp.Replace(",", "."), xr.Replace(",", "."), yr.Replace(",", "."), zr.Replace(",", "."), wr.Replace(",", ".")));
            //pointsStringList.Add(string.Format("{0}, {1}", vector, quaternion));
        }
    }

(NB: please note that “string xp = pointsPosition*.x.ToString();” outputs Vector3 x value with kommas instead of periods, which is why I use Replace(“,”, “.”) so that the strings I add to be saved contains “.” instead of “,”'s in the Json file, so they cannot just be saved but also easy reloaded. Must be a Unity thing…)*
the list “stringsToSave” is declared as followed (in the start of my TestSimulation script)
```csharp

  • public static List stringsToSave = new List();*
    * *and then saving the "listToSave" via my SavingService script using:* *csharp
  •    // Get the list of points.
      var points = data[POINTS_STRING_KEY];
      int pointsCount = points.Count;
    
      // Check if there are points in the save file.
      if (pointsCount == 0)
      {
          Debug.LogWarningFormat("Data at {0} does not contain any points.");
      }
      else
      {
          // Split the points and add them to a List of strings.
          for (int i = 0; i < pointsCount; i++)
          {
              LOADED_POINTS.Add((string)points[i]);
          }
      }*
    

* *where "POINTS_STRING_KEY" references (in the "SavingService" script):* *csharp

  • private const string POINTS_STRING_KEY = “points”;*
    * *resulting in a Json file looking like this;* *csharp
    {
    “objects” : [
    {
    “localPosition” : {
    “x” : 0.0,
    “y” : 0.0200079996138811,
    “z” : 8.70414726109344E-25
    },
    “localRotation” : {
    “x” : 0.0,
    “y” : 0.0,
    “z” : 0.0,
    “w” : 1.0
    },
    “localScale” : {
    “x” : 1.0,
    “y” : 1.0,
    “z” : 1.0
    },
    “$saveID” : “c5909a20-d055-498c-9f21-74d0d1ddbaf3”
    }
    ],
    “scenes” : [
    “GetPenUniverse”
    ],
    “activeScene” : “GetPenUniverse”,
    “points” : [
    “0,1,0,0,0,0,1”,
    “1,1,0,0.3826835,0,0,0.9238795”,
    “1,1,1,0,0,0,1”,
    “2,2,2,0.45,0,0,1”,
    “3,3,3,0.2164396,0,0,0.976296”,
    “0,0,0,0,0,0,1”,
    “7.5,0,0,0.3826835,0,0,0.9238795”,
    “14,0,0,-0.3826835,0,0,0.9238795”
    ]
    }

    ```

All looks good and you may leave it as is if working as expected. But theres one part I suggest you don’t don like this anymore. Where you combining your string. It will not bring you a lot of trouble in this code, but may affect performace if you use this technique often later, especially in gameplay code. Instead of combining strings, use StringBuilder class. With this your code will be a little more effective in ter

1 Like

I will certainly look into that :smiley:
Thank you for your inputs they helped a lot :smiley:
Keep up the good work :smiley: