Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Saving and loading several objects

Discussion in 'Scripting' started by RoyalKingZB, Jun 22, 2022.

  1. RoyalKingZB

    RoyalKingZB

    Joined:
    Sep 23, 2021
    Posts:
    4
    Hi, I've been looking at youtube videos on saving and loading objects in a room and this is what I found. This saves and loads the position of a specific object type(chest) when leaving and entering the scene. However, I can't figure out how to make this work with say, a whole list of items that can be placed. It doesn't seem very obvious to me but I might just be dumb. I wouldn't have to duplicate this entire thing for each object right?

    Main Save Load objects placed class=

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3. using System.IO;
    4. using System.Runtime.Serialization.Formatters.Binary;
    5. using UnityEngine.SceneManagement;
    6.  
    7. public class SaveObjects : MonoBehaviour
    8. {
    9.     [SerializeField] GameObject chestPrefab;
    10.  
    11.     public static List<GameObject> chests = new List<GameObject>();
    12.  
    13.     const string SAVEDOBJECTS_SUB = "/savedObjects";
    14.     const string SAVEDOBJECTS_COUNT_SUB = "/savedObjects.count";
    15.  
    16.     void Awake()
    17.     {
    18.         LoadAll();
    19.     }
    20.  
    21.     void OnApplicationQuit()
    22.     {
    23.         SaveAll();
    24.     }
    25.  
    26.     public void SaveAll()
    27.     {
    28.         SaveChest();
    29.     }
    30.  
    31.     public void LoadAll()
    32.     {
    33.         LoadChest();
    34.     }
    35.  
    36.     public void SaveChest()
    37.     {
    38.         BinaryFormatter formatter = new BinaryFormatter();
    39.         string path = Application.persistentDataPath + SAVEDOBJECTS_SUB +  SceneManager.GetActiveScene().buildIndex;
    40.         string countPath = Application.persistentDataPath + SAVEDOBJECTS_COUNT_SUB + SceneManager.GetActiveScene().buildIndex;
    41.  
    42.         FileStream countStream = new FileStream(countPath, FileMode.Create);
    43.  
    44.         formatter.Serialize(countStream, chests.Count);
    45.         countStream.Close();
    46.  
    47.         for (int i = 0; i < chests.Count; i++)
    48.         {
    49.             FileStream stream = new FileStream(path + i, FileMode.Create);
    50.             ChestData data = new ChestData(chests[i]);
    51.  
    52.             formatter.Serialize(stream, data);
    53.             stream.Close();
    54.         }
    55.     }
    56.  
    57.     public void LoadChest()
    58.     {
    59.         BinaryFormatter formatter = new BinaryFormatter();
    60.         string path = Application.persistentDataPath + SAVEDOBJECTS_SUB + SceneManager.GetActiveScene().buildIndex;
    61.         string countPath = Application.persistentDataPath + SAVEDOBJECTS_COUNT_SUB + SceneManager.GetActiveScene().buildIndex;
    62.         int chestCount = 0;
    63.  
    64.         if(File.Exists(countPath))
    65.         {
    66.             FileStream countStream = new FileStream(countPath, FileMode.Open);
    67.  
    68.             chestCount = (int)formatter.Deserialize(countStream);
    69.             countStream.Close();
    70.         }
    71.         else
    72.         {
    73.             Debug.LogError("Path not found in " + countPath);
    74.         }  
    75.  
    76.         for (int i = 0; i < chestCount; i++)
    77.         {
    78.             if(File.Exists(path + i))
    79.             {
    80.                 FileStream stream = new FileStream(path + i, FileMode.Open);
    81.                 ChestData data = formatter.Deserialize(stream) as ChestData;
    82.                
    83.                 stream.Close();
    84.  
    85.                 Vector3 position = new Vector3(data.position[0], data.position[1], data.position[2]);
    86.  
    87.                 GameObject chest = Instantiate(chestPrefab, position, Quaternion.identity);
    88.             }
    89.             else
    90.             {
    91.                 Debug.LogError("Path not found in " + path + i);
    92.             }
    93.         }
    94.     }
    95. }
    96.  
    Chest Data class=

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [System.Serializable]
    4. public class ChestData
    5. {
    6.     public float[] position = new float[3];
    7.  
    8.     public ChestData(GameObject chest)
    9.     {
    10.         if (chest)
    11.         {
    12.             Vector3 chestPos = chest.transform.position;
    13.  
    14.             position[0] = chestPos.x;
    15.             position[1] = chestPos.y;
    16.             position[2] = chestPos.z;
    17.  
    18.             position = new float[]
    19.             {
    20.             chestPos.x, chestPos.y, chestPos.z
    21.             };
    22.         }
    23.     }
    24. }
    25.  
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,016
    Compulsory warning as to why you shouldn't use binary formatter. Is this tutorial telling you to use it? You should be using JSON.

    In any case you could look to use interfaces here. Have an Interface that outlines the information you need in order to save them, and a simple FindObjectsOfType<> can grab a reference to everything with this interface and save the needed information.
     
  3. RoyalKingZB

    RoyalKingZB

    Joined:
    Sep 23, 2021
    Posts:
    4
    I'm sorry but can you elaborate and maybe provide some examples? Save load systems hurt my brain Lmfao
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,016
    Save systems are difficult when you're starting out, I agree. Though it's hard to provide code examples as there's a lot of moving parts to them, but I'll sling some concepts.

    I suggest looking into Interfaces nonetheless. They're effectively a contract that an object agrees to by implementing it. When you think about it, every object you need to save will need to provide the same data.

    Say you have this interface:
    Code (CSharp):
    1. public interface ISaveable
    2. {
    3.     public string GUID { get; }
    4.  
    5.     public Vector3 Position { get; set; }
    6.  
    7.     public Quaternion Rotation { get; set; }
    8. }
    The GUID, aka a unique identifier only needs to be read-only. The other two properties need to be read/write.

    Thus, a monobehaviour that implements it could look like this:
    Code (CSharp):
    1. public class SomeMonobehaviour : Monobehaviour, ISaveable
    2. {
    3.     [SerializeField, HideInInspector]
    4.     private string guid;
    5.  
    6.     public string GUID => guid;
    7.  
    8.     public Vector3 Position { get { return this.transform.position; } set { this.transform.position = value; } }
    9.  
    10.     public Quaternion Rotation { get { return this.transform.rotation; } set { this.transform.rotation = value; } }
    11.  
    12. //this code is only for the editor and automatically assigns the GUID
    13. #if UNITY_EDITOR
    14.     public void Reset() //called when component is added
    15.     {
    16.         guid = System.Guid.NewGuid().ToString();
    17.         UnityEditor.EditorUtility.SetDirty(this);
    18.     }
    19. #endif
    20. }
    And since you have an interface that defines the properties that all saveable game objects must implement, you can make a serializable struct that takes the interface as a parameter of its constructor. This way, we really only need the one struct for saving game objects in this manner:
    Code (CSharp):
    1. [System.Serializable]
    2. [JsonObject(MemberSerialization = MemberSerialization.OptIn)]
    3. public struct SaveableObjectData
    4. {
    5.     public SaveableObjectData(ISaveable saveable)
    6.     {
    7.         GUID = saveable.GUID;
    8.         Position = new Vector3_S(saveable.Position);
    9.         Rotation = new Quaternion_S(saveable.Rotation);
    10.     }
    11.    
    12.     [JsonProperty]
    13.     public string GUID { get; private set; }
    14.    
    15.     [JsonProperty]
    16.     public Vector3_S Position { get; private set; }
    17.    
    18.     [JsonProperty]
    19.     public Quaternion_S Rotation { get; private set; }
    20. }
    The above is set up for using Newtonsoft.JSON, with my preference of opting members in for serialisation (I prefer to be explicit). Vector3_S and Quaternion_S are made up structs you would need to implement as the built in Vector3/Quaternion structs don't play nice with JSON serialisation. These objects are generally known as serialisation surrogates.

    So with all this, the brute force method of saving everything in a scene would be to use
    FindObjectsOfType<ISaveable>()
    and make an instance of SaveableObjectData for everything we find and write that to the save data we're going to serialise out. Then on loading, you deserialise this data, grab everything again with ISaveable, then for every instance of SaveableObjectData you find its matching game object and assign the position and rotation to it.

    Obviously that's not particularly efficient, but you get the idea.