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. Dismiss Notice

Question Saving/Loading groups and lists of variable length?

Discussion in 'Scripting' started by scoopz, Jun 13, 2023.

  1. scoopz

    scoopz

    Joined:
    Feb 24, 2022
    Posts:
    4
    Howdy,

    So what we are looking at is just a mock up. We have a player character that moves. Dummy NPCs. And a cube.

    What I want to do is take the player character, a list of unique NPC followers, save their properties, and every time you go from the main menu -> to level/world/gameplay scene load those in and have them spawn in a radius around a certain spawnpoint (the cube).

    Now, following this video:
    which was recommended to me by the helpful Unity Discord, I was able to write a script that allows saving and loading the properties of a single GameObject. The Player Character. It looks like this:
    Code (CSharp):
    1.  using UnityEngine;
    2. using System.IO;
    3. using System.Runtime.Serialization.Formatters.Binary;
    4.  
    5. public static class SaveSystem
    6. {
    7.  
    8.     //Player
    9.     public static void SaveActor (Actor_BaseScript actor)
    10.     {
    11.  
    12.         BinaryFormatter formatter = new BinaryFormatter();
    13.         string path = Application.persistentDataPath + "/actor_save.sol";
    14.         FileStream stream = new FileStream(path, FileMode.Create);
    15.  
    16.         ActorData data = new ActorData(actor);
    17.  
    18.         formatter.Serialize(stream, data);
    19.         stream.Close();
    20.  
    21.     }
    22.  
    23.     public static ActorData LoadActor ()
    24.     {
    25.  
    26.         string path = Application.persistentDataPath + "/actor_save.sol";
    27.         if (File.Exists(path))
    28.         {
    29.             BinaryFormatter formatter = new BinaryFormatter();
    30.             FileStream stream = new FileStream(path, FileMode.Open);
    31.  
    32.             ActorData data = formatter.Deserialize(stream) as ActorData;
    33.             stream.Close();
    34.  
    35.             return data;
    36.         }
    37.         else
    38.         {
    39.             Debug.LogError("Save file not found in " + path);
    40.             return null;
    41.         }
    42.  
    43.     }
    44.  
    45.     // NPCs
    46.     public static void SaveNPC (ActorNPC_BaseScript npc)
    47.     {
    48.  
    49.         BinaryFormatter formatter = new BinaryFormatter();
    50.         string path = Application.persistentDataPath + "/npc_save.sol";
    51.         FileStream stream = new FileStream(path, FileMode.Create);
    52.  
    53.         ActorNPCData data = new ActorNPCData(npc);
    54.  
    55.         formatter.Serialize(stream, data);
    56.         stream.Close();
    57.  
    58.     }
    59.  
    60.     public static ActorNPCData LoadNPC ()
    61.     {
    62.  
    63.         string path = Application.persistentDataPath + "/npc_save.sol";
    64.         if (File.Exists(path))
    65.         {
    66.             BinaryFormatter formatter = new BinaryFormatter();
    67.             FileStream stream = new FileStream(path, FileMode.Open);
    68.  
    69.             ActorNPCData data = formatter.Deserialize(stream) as ActorNPCData;
    70.             stream.Close();
    71.  
    72.             return data;
    73.         }
    74.         else
    75.         {
    76.             Debug.LogError("Save file not found in " + path);
    77.             return null;
    78.         }
    79.  
    80.     }
    81.  
    82.     // Group
    83.     public static void SaveGroup (ActorGroup_BaseScript group)
    84.     {
    85.  
    86.         BinaryFormatter formatter = new BinaryFormatter();
    87.         string path = Application.persistentDataPath + "/group_save.sol";
    88.         FileStream stream = new FileStream(path, FileMode.Create);
    89.  
    90.         ActorGroupData data = new ActorGroupData(group);
    91.  
    92.         formatter.Serialize(stream, data);
    93.         stream.Close();
    94.  
    95.     }
    96.  
    97.     public static ActorGroupData LoadGroup ()
    98.     {
    99.  
    100.         string path = Application.persistentDataPath + "/group_save.sol";
    101.         if (File.Exists(path))
    102.         {
    103.             BinaryFormatter formatter = new BinaryFormatter();
    104.             FileStream stream = new FileStream(path, FileMode.Open);
    105.  
    106.             ActorGroupData data = formatter.Deserialize(stream) as ActorGroupData;
    107.             stream.Close();
    108.  
    109.             return data;
    110.         }
    111.         else
    112.         {
    113.             Debug.LogError("Save file not found in " + path);
    114.             return null;
    115.         }
    116.  
    117.     }
    118.  
    119. }
    Which works well enough. I tested it by calling it through a script on the player character. You can also see I unhelpfully duplicated the functions for NPCs and "Groups" (i.e. the theoretical squad/platoon/company etc.). At this point I am getting lost. I am very confused about:

    -- How to set up a prefab and what to store in it
    -- How to instantiate a number of objects (the NPCs) and load their data from a file.
    -- How to tie this to a physical spawn object so that every time you enter a scene you instantiate 'Your Dudes' around that object.

    --It's also important that the player character gameobject and the NPC gameobjects are associated with a top level thing called a group. So that as old NPCs die and new ones get bought between sessions, the group remains contiguous and tracks various metrics.

    So yea, I am missing a lot. This ended up being a HUGE chunk and I need some help breaking it down into smaller bite-sized pieces to figure out. Thanks very much in advance for the help <3!
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    First, why you should not using the binary formatter: https://learn.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide

    Any tutorial suggesting the binary formatter is either old, out of date, or just a bad tutorial. Nowadays you should use either Unity's Json Utility (not recommended as its so limited), Newtonsoft.JSON, or the Odin Serialiser.

    Its also important to remember you cannot serialise out anything inheriting from
    UnityEngine.Object
    , and deserialise it later, neither can you do the same for references to Unity objects as well. Any attempt to do so is a dead end.

    The trick to saving is really just to wrap up the data you care about into a class, write out that data, and then load it when you need it to reconstruct the runtime data. You will very often need to convert runtime Unity data (namely object references) with substitute data (often called a serialisation surrogate), such as serialising out some kind of UUID or even just the name of the asset, and use that ID to look up the asset from a database at a later point in time.

    So in your case, you likely just want a class that wraps around a collection of a data type. Said type probably wants to record some kind of ID that can be used to look up the respective follower prefab, along with any relevant ancillary data (health, stats, whatever).

    Its something you're going to have to put in the hours to get your head around. Start with a smaller scope test project to play around with it.
     
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    This is not intended to replace or invalidate anything spiney199 said above. All valid points.
    That said, I trust this to be a good tutorial on doing binary savegames in a post-BinaryFormatter world:
    Fixing Brackeys' Save & Load System

    Another blog post also deals with Protocol Buffers. Nice blog!
     
    eses, scoopz and Ryiah like this.
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,561
    Adding onto Spiney and Orion, here's my feverish scribblings and hoarded links about the massively over-thought area of game engineering known as...

    Load/Save steps:

    https://forum.unity.com/threads/save-system-questions.930366/#post-6087384

    An excellent discussion of loading/saving in Unity3D by Xarbrough:

    https://forum.unity.com/threads/save-system.1232301/#post-7872586

    Loading/Saving ScriptableObjects by a proxy identifier such as name:

    https://forum.unity.com/threads/use...lds-in-editor-and-build.1327059/#post-8394573

    When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls
    new
    to make one, it cannot make the native engine portion of the object.

    Instead you must first create the MonoBehaviour using AddComponent<T>() on a GameObject instance, or use ScriptableObject.CreateInstance<T>() to make your SO, then use the appropriate JSON "populate object" call to fill in its public fields.

    If you want to use PlayerPrefs to save your game, it's always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

    https://gist.github.com/kurtdekker/7db0500da01c3eb2a7ac8040198ce7f6

    Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

    https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide
     
    scoopz, orionsyndrome and Ryiah like this.
  5. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    20,082
    You can with a serialization system that respects Unity like Odin Serializer. You won't get an object that can be seen from the Inspector nor will you get an asset in your folders but you will get an instance in memory.

    Code (CSharp):
    1. using Sirenix.Serialization;
    2. using System.IO;
    3. using UnityEngine;
    4.  
    5. public class MyMonoBehaviour : MonoBehaviour
    6. {
    7.     public MyScriptableObject scriptableObject;
    8.  
    9.     void Start()
    10.     {
    11.         byte[] bytes = File.ReadAllBytes("foo.txt");
    12.         scriptableObject = SerializationUtility.DeserializeValue<MyScriptableObject>(bytes, DataFormat.JSON);
    13.  
    14.         Debug.Log(scriptableObject.myInt);
    15.         Debug.Log(scriptableObject.myFloat);
    16.         Debug.Log(scriptableObject.myString);
    17.     }
    18. }
    19.  
    20. [CreateAssetMenu]
    21. public class MyScriptableObject : ScriptableObject
    22. {
    23.    public int myInt;
    24.    public float myFloat;
    25.    public string myString;
    26. }

    Code (csharp):
    1. {
    2.     "$id": 0,
    3.     "$type": "0|MyScriptableObject, Assembly-CSharp",
    4.     "myInt": 3,
    5.     "myFloat": 1415,
    6.     "myString": "Foo"
    7. }

    upload_2023-6-13_13-30-1.png

    upload_2023-6-13_13-31-49.png
     
    Last edited: Jun 13, 2023
    scoopz likes this.
  6. scoopz

    scoopz

    Joined:
    Feb 24, 2022
    Posts:
    4
    Thanks everyone for your helpful replies! Looks like I've got some work to do! Time make some DATA STRUCTURES
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    I want to bring up this thread again and point out that you should not do this. Unity objects are not meant to be serialised out and deserialised, and something the Odin devs recommend not to do.

    To quote one of the main dev's Tor, earlier today:
    Bottom line is you should always be serialising a plain C# class over a Unity object.
     
    Ryiah likes this.