Search Unity

One Master Savel class or Save Load Constructor on objects?

Discussion in 'Scripting' started by Unlimited_Energy, Sep 6, 2020.

  1. Unlimited_Energy

    Unlimited_Energy

    Joined:
    Jul 10, 2014
    Posts:
    469
    Just wondering what you all recommend as a save/load pattern. Should I save/load from one master class that references every variable in the game that needs to be saved/loaded or should I have the save/load be performed inside each class on my components with the serialization of each class when needed being called from a master serialization/de serialization class. I was going to follow brackeys save load system but wanted you guys thoughts.I'm actually curious if anyone works or has worked at a game studio what method you guys would use?
     
  2. Terraya

    Terraya

    Joined:
    Mar 8, 2018
    Posts:
    646
    Well,

    that depends on your Game and its scale i would say,
    we are currently developing an RPG and saving goes through ea. Entity which have a unique ID for example.

    if you have a smaller game , it realy depends


    EDIT: Well i miss spelled my text i think,
    we got a ISaveable Script on ea. entity component which should get saved,
    at point of saving we are getting all ISaveable Components into a List / Dicctionary to save
     
    Last edited: Sep 7, 2020
    Unlimited_Energy likes this.
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    I'd argue that letting things save themselves is more scalable.

    What I do is that I have a datacontainer type (personally call mine ScenarioPersistentData), it can have key/value pairs added to it, and is serializable. The serialization engine I use is NOT the Unity built in one (since it requires a strongly typed class structure... I use an engine that will record the datatype of the object, so if a the variable/field is type 'object' but I put an 'int' in there it knows how to deal with that... JSON.Net can operate in this mode if you tell it to store the type information).

    When I go to save the game I dispatch an event to everything in the scene. This event passes along a reference to this datacontainer, and everything can shove data into it of its own accord. Usually it's some sort of token object of its own and the key is a uid for that object.

    Here's an example:
    (I've snipped a lot of the implementation of this object, just the variables and the save/load handlers... the 'save' logic is towards the bottom of the class)
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.UI;
    4. using System.Collections.Generic;
    5. using System.Linq;
    6.  
    7. using com.spacepuppy;
    8. using com.spacepuppy.Collections;
    9. using com.spacepuppy.Scenario;
    10. using com.spacepuppy.SPInput;
    11. using com.spacepuppy.Tween;
    12. using com.spacepuppy.Utils;
    13.  
    14. using com.mansion.Entities.Inventory;
    15. using com.mansion.Entities.UI;
    16. using com.mansion.UserInput;
    17.  
    18. namespace com.mansion.Scenarios.Episode2
    19. {
    20.  
    21.     public class Ep2_Forge : SPComponent, IMPersistentObject
    22.     {
    23.  
    24.         [System.Flags]
    25.         public enum ForgeStates
    26.         {
    27.             Empty = 0,
    28.             ContainsItem = 1,
    29.             ContainsMold = 2,
    30.             ContainsMoldAndItem = 3,
    31.             ContainsResult = 4,
    32.             On = 8,
    33.             CurrentlyMeltingItem = 9,
    34.             CurrentlyMeltingNothingWithMold = 10,
    35.             CurrentlyMeltingItemIntoMold = 11
    36.         }
    37.      
    38.         #region Fields
    39.  
    40.         [SerializeField]
    41.         [TokenId.Config(AllowZero = true)]
    42.         private TokenId _uid;
    43.  
    44.         [SerializeField]
    45.         [ReadOnly]
    46.         private ForgeStates _forgeState;
    47.         [SerializeField]
    48.         [ReadOnly]
    49.         private InventoryItem _furnaceContents;
    50.         [SerializeField]
    51.         [ReadOnly]
    52.         private InventoryItem _groundContents;
    53.      
    54.         [SerializeField]
    55.         [DisableOnPlay]
    56.         [ReorderableArray()]
    57.         [DefaultFromSelf(EntityRelativity.SelfAndChildren)]
    58.         [InsertButton("Find Recipes", "AutoSearchRecipesInSelf", PrecedeProperty = false)]
    59.         private Ep2_ForgeRecipe[] _recipes;
    60.  
    61.         [Space(10)]
    62.         [Header("Events")]
    63.         [SerializeField]
    64.         private bool _displayDefaultRetrievalMessage = true;
    65.         [SerializeField]
    66.         private Trigger _onRequestedItemCancelled;
    67.         [SerializeField]
    68.         private Trigger _onRequestedItemFailed;
    69.         [SerializeField]
    70.         private Trigger _onRetrieveItemCriticalFail;
    71.         [SerializeField]
    72.         private Trigger _onRetrieveItemButInventoryFull;
    73.         [SerializeField]
    74.         private Trigger _onFailedToStartForge;
    75.         [SerializeField]
    76.         private Trigger _onStartForge;
    77.  
    78.         [System.NonSerialized]
    79.         private InventoryItem[] _availableItems;
    80.         [System.NonSerialized]
    81.         private HashSet<InventoryItem> _molds = new HashSet<InventoryItem>();
    82.  
    83.         [System.NonSerialized]
    84.         private RadicalCoroutine _requestItemRoutine;
    85.  
    86.         #endregion
    87.  
    88.         #region CONSTRUCTOR
    89.  
    90.         protected override void Awake()
    91.         {
    92. //snip
    93.         }
    94.      
    95.         #endregion
    96.  
    97.         #region Properties
    98.  
    99.         public ForgeStates ForgeState
    100.         {
    101.             get { return _forgeState; }
    102.         }
    103.  
    104.         public InventoryItem FurnaceContents
    105.         {
    106.             get { return _furnaceContents; }
    107.         }
    108.      
    109.         public InventoryItem GroundContents
    110.         {
    111.             get { return _groundContents; }
    112.         }
    113.  
    114.         #endregion
    115.  
    116.         #region Methods
    117.  
    118.         public void RequestItemFromPlayer()
    119.         {
    120. //snip
    121.         }
    122.  
    123.         public void RemoveItemAndGiveToPlayer()
    124.         {
    125. //snip
    126.         }
    127.  
    128.         public void TurnOnFire()
    129.         {
    130. //snip
    131.         }
    132.  
    133.         public void SignalSmeltingComplete()
    134.         {
    135. //snip
    136.         }
    137.  
    138.  
    139.  
    140.         private System.Collections.IEnumerator DoRequestItem(IInventoryDisplay inventory)
    141.         {
    142. //snip
    143.         }
    144.  
    145.         private Ep2_ForgeRecipe TryFindRecipe(InventoryItem item, bool searchItem, bool searchMold, bool searchResult)
    146.         {
    147. //snip
    148.         }
    149.  
    150.         private Ep2_ForgeRecipe FindExactRecipe(InventoryItem item, InventoryItem mold)
    151.         {
    152. //snip
    153.         }
    154.  
    155.         private void NormalizeForgeState()
    156.         {
    157. //snip
    158.         }
    159.  
    160.         private void AutoSearchRecipesInSelf()
    161.         {
    162.             _recipes = this.GetComponentsInChildren<Ep2_ForgeRecipe>();
    163.         }
    164.  
    165.         #endregion
    166.  
    167.         #region IPersistentObject Interface
    168.  
    169.         void IPersistentObject.OnSceneLoaded(ScenarioPersistentData data, LoadReason reason)
    170.         {
    171.             //do nothing
    172.         }
    173.  
    174.         void IPersistentObject.OnLoad(ScenarioPersistentData data, LoadReason reason, LoadStatus status)
    175.         {
    176.             if (!_uid.HasValue || data == null || Game.EpisodeSettings == null) return;
    177.  
    178.             var items = Game.EpisodeSettings.InventorySet;
    179.             if (object.ReferenceEquals(items, null)) return;
    180.  
    181.             Token token;
    182.             if (!data.TryGetData<Token>(_uid.ToString(), out token)) return;
    183.  
    184.             _furnaceContents = (token != null && !string.IsNullOrEmpty(token.FurnaceContent)) ? items.GetAsset(token.FurnaceContent) as InventoryItem : null;
    185.             _groundContents = (token != null && !string.IsNullOrEmpty(token.GroundContent)) ? items.GetAsset(token.GroundContent) as InventoryItem : null;
    186.             this.NormalizeForgeState();
    187.  
    188.             var recipe = this.FindExactRecipe(_furnaceContents, _groundContents);
    189.             if (recipe != null)
    190.             {
    191.                 recipe.OnItemReloaded.ActivateTrigger(this, null);
    192.                 recipe.OnMoldReloaded.ActivateTrigger(this, null);
    193.             }
    194.             else
    195.             {
    196.                 recipe = this.TryFindRecipe(_furnaceContents, true, false, false);
    197.                 if (recipe != null)
    198.                 {
    199.                     recipe.OnItemReloaded.ActivateTrigger(this, null);
    200.                 }
    201.  
    202.                 recipe = this.TryFindRecipe(_groundContents, false, true, true);
    203.                 if (recipe != null)
    204.                 {
    205.                     if (recipe.OptionalMold == _groundContents)
    206.                         recipe.OnMoldReloaded.ActivateTrigger(this, null);
    207.                     else if (recipe.Result == _groundContents)
    208.                         recipe.OnResultReloaded.ActivateTrigger(this, null);
    209.                 }
    210.             }
    211.         }
    212.  
    213.         void IPersistentObject.OnSave(ScenarioPersistentData container, SaveReason reason)
    214.         {
    215.             if (_uid.HasValue)
    216.             {
    217.                 container.SetData<Token>(_uid.ToString(), new Token()
    218.                 {
    219.                     FurnaceContent = (_furnaceContents != null) ? _furnaceContents.name : null,
    220.                     GroundContent = (_groundContents != null) ? _groundContents.name : null,
    221.                     State = _forgeState
    222.                 });
    223.             }
    224.         }
    225.      
    226.         #endregion
    227.  
    228.         #region Special Types
    229.  
    230.         [System.Serializable]
    231.         public class Token
    232.         {
    233.             public string FurnaceContent;
    234.             public string GroundContent;
    235.             public ForgeStates State;
    236.         }
    237.      
    238.         #endregion
    239.  
    240.     }
    241.  
    242. }
    243.  
    So the IPersistentObject interface is how my event dispatching system works. I have an event system that finds all objects of an interface type and calls respective events on it (I can also add them to poll manually for a non-global version).

    So in this case when the save system says to load a save file this thing receives the datacontainer, it gets its token out of it, and it establishes its state accordingly. In the case of this object, it represent a special "forge" in our puzzle game that you can put items into and smelt them into other objects. This is supposed to handle if you leave an object in it, or walk away while it's running and leave the scene.

    And of course the save just puts that token into the data container.

    Here you can see where I dispatch that 'save' event:
    Code (csharp):
    1.  
    2.         public override void SaveScenario(SaveReason reason, bool saveToDisk)
    3.         {
    4.             if(saveToDisk && Game.EpisodeSettings.Id != Episode.Unknown)
    5.             {
    6.                 var stats = Services.Get<com.mansion.Statistics.GameStatistics>();
    7.                 if (stats != null) stats.AdjustStat(com.mansion.Statistics.GameStatistics.STAT_SAVEDGAME, 1f);
    8.  
    9.                 var data = Services.Get<ScenarioPersistentData>();
    10.                 if (data != null)
    11.                 {
    12.                     Messaging.Broadcast<IPersistentObject>((o) => o.OnSave(data, reason));
    13.                     _stateToken = data.CreateStateToken();
    14.                 }
    15.  
    16.                 var token = new SaveGameToken()
    17.                 {
    18.                     Data = this.StateToken,
    19.                     Checkpoint = this.Checkpoint
    20.                 };
    21.  
    22.                 token.Save(System.IO.Path.Combine(Application.persistentDataPath, Constants.GetSaveFileName(Game.EpisodeSettings.Id)));
    23.             }
    24.             else
    25.             {
    26.                 var data = Services.Get<ScenarioPersistentData>();
    27.                 if (data != null)
    28.                 {
    29.                     Messaging.Broadcast<IPersistentObject>((o) => o.OnSave(data, reason));
    30.                     _stateToken = data.CreateStateToken();
    31.                 }
    32.             }
    33.         }
    34.  
    (note this save system storestodisk only if we're saving to disk. But we also save to memory between scenes to save state as you walk around the world. It doesn't always get written to disk to add difficulty to the game... if in "Hard/Classic" mode you have to go feed kitties to actually save to disk)

    And here is the SaveToken itself and its Save/Load serialization methods:
    Code (csharp):
    1.  
    2.  
    3.     [System.Serializable]
    4.     public class SaveGameToken
    5.     {
    6.         //public string Scene;
    7.         public object Data;
    8.         public CheckpointState Checkpoint;
    9.  
    10.  
    11.         public void Save(string filename)
    12.         {
    13.             try
    14.             {
    15.                 using (var strm = new MemoryStream())
    16.                 using (var serializer = com.spacepuppy.Serialization.SPSerializer.Create())
    17.                 {
    18.                     var formatter = new com.spacepuppy.Serialization.Json.JsonFormatter();
    19.                     serializer.Serialize(formatter, strm, this);
    20.                  
    21.                     using (var file = new System.IO.FileStream(filename, FileMode.Create, FileAccess.Write))
    22.                     {
    23.                         strm.Position = 0;
    24.                         var arr = strm.ToArray();
    25.                         file.Write(arr, 0, arr.Length);
    26.                     }
    27.                 }
    28.             }
    29.             catch(System.Exception ex)
    30.             {
    31.                 //TODO - display some type of warning
    32.                 UnityEngine.Debug.LogException(ex);
    33.             }
    34.         }
    35.  
    36.         public static SaveGameToken Load(string filename)
    37.         {
    38.             try
    39.             {
    40.                 if (!File.Exists(filename)) return null;
    41.                 using (var strm = new FileStream(filename, FileMode.Open, FileAccess.Read))
    42.                 using (var serializer = com.spacepuppy.Serialization.SPSerializer.Create())
    43.                 {
    44.                     var formatter = new com.spacepuppy.Serialization.Json.JsonFormatter();
    45.                     return serializer.Deserialize(formatter, strm) as SaveGameToken;
    46.                 }
    47.             }
    48.             catch(System.Exception ex)
    49.             {
    50.                 UnityEngine.Debug.LogException(ex);
    51.                 return null;
    52.             }
    53.         }
    54.      
    55.         public static bool FileIsValid(string path)
    56.         {
    57.             try
    58.             {
    59.                 if (!System.IO.File.Exists(path)) return false;
    60.  
    61.             using (var reader = new StreamReader(path))
    62.             {
    63.                 string line = reader.ReadLine();
    64.                 if (line == null || !line.StartsWith("{")) return false;
    65.  
    66.                 line = reader.ReadLine();
    67.                 return line != null && line.Contains("com.mansion.SaveGameToken");
    68.                 }
    69.             }
    70.             catch (System.Exception ex)
    71.             {
    72.                 UnityEngine.Debug.LogException(ex);
    73.                 return false;
    74.             }
    75.         }
    76.  
    77.         public static void WriteInvalidSaveToken(string path)
    78.         {
    79.             try
    80.             {
    81.                 System.IO.File.WriteAllText(path, string.Empty);
    82.             }
    83.             catch (System.Exception ex)
    84.             {
    85.                 UnityEngine.Debug.LogException(ex);
    86.             }
    87.         }
    88.  
    89.     }
    90.  
    As you can see I use my own serialization engine "SPSerializer". I could use JSON.Net here if I wanted. I just can't use JsonUtility because it wouldn't respect the fact that 'object Data' is an 'object'.