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. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

C#: Saving Data of Objects in a Scene - Metroidvania Style

Discussion in 'Scripting' started by Unfurl_Games, Jul 7, 2018.

  1. Unfurl_Games

    Unfurl_Games

    Joined:
    Jan 4, 2014
    Posts:
    16
    Hey!

    Now I know there is a ton of different ways to do this and I have already implemented a save system for the player character(using sterializable fields and binary formats) but I am pretty lost on where to start with saving things like switches for doors in other scenes, interactable objects like a tree being knocked down. I know that leaving the scene and coming back will just reload the scene from its default state.

    The project I am working on is basically a 2D Metroidvania. So there will need to be data saved for mostly all scenes, this is pretty important for metroidvanias because if a player breaks a platform to create a bridge, that data needs to be saved.

    I considered making the entire game in one scene (excluding main menu) and having scenes activate and deactivate when you go between them, keeping data persistent. While this seems like a pretty good idea, the whole game still needs to be saved when the player exits and comes back to the game.

    I've seen a ton of tutorials about this so I know there are many different methods of saving data, but isn't there a more clear way of saving 'interactable' objects or like data related to in-game events (like ambushes or story interactions)

    Should I just have every intractable script save like the player does? Can it all just be saved into one file? If I've got 50 levers in total, that's a ton of different types of data to keep track of. Do I just give IDs to all of the interactable objects and save them using their ids (not sure how you'd do that)?

    Did games like Hollow Knight and Metroid (obviously wasn't built with unity but the same logic) use different scenes for each room and did they also save their scenes data like you would with the player's script?

    Sorry for the wall of text, I'm just trying to understand the logic of saving a lot of the same or different scripts for interactable objects. Thanks for your time!
     
    TreeHacks and RemDust like this.
  2. eses

    eses

    Joined:
    Feb 26, 2013
    Posts:
    2,637
    Hi @Unfurl_Games

    Interesting question, but I fear this kind of post won't get too many answers - Not showing something you already have makes it a bit hard to answer, it's really quite open-ended and big question. It would be really nice to see something first, even pseudocode or links to best articles you found.

    I'm pretty much in the same situation. I haven't created such system yet (although I'm creating one) but I think I'd do something like this;

    1. Have save slots (save0_, save1_, ...)
    2. Have save file for each area (save0_area0, save0_area1, ...)
    3. Load level / area when needed (entering area etc)
    4. Collect all saveable objects (Then destroy them from level optionally if you create all in next the step)
    5. Use load data to recreate (or set state) all objects that can have state (loot, levers, doors, collectables)
    6. Play game
    7. When saving, do the reverse; save to slot/area all previous types of items currently in level

    There are many tutorials and articles available, you say you have read a ton of tutorials (but which ones? You didn't mention) so maybe just try something first, then post again.

    "Did games like Hollow Knight and Metroid (obviously wasn't built with unity but the same logic) use different scenes for each room"

    There is an article about level creation for Hollow Knight, didn't read it properly - but here is the link: https://www.pcgamer.com/how-to-design-a-great-metroidvania-map/
     
    Last edited: Jul 7, 2018
  3. Unfurl_Games

    Unfurl_Games

    Joined:
    Jan 4, 2014
    Posts:
    16
    Thanks for the info eses! I did actually read that article a 5 or so days ago, super interesting but they don't go into specific technical details like this really.

    Sorry for the really open ended and large question, I'm not exactly looking for a large chunk of code but rather the step by step logic behind it (your steps helped!).I spent the better part of a day trying to make saving work and I managed to get it going however when it comes to saving specific data it would overwrite the rest of the data in the file.

    I ended up just using a free asset on the unity store which made saving sooo much easier.

    Most people I'ved talk to said its probably a good idea to have all if not most of the game in one large scene but enable and disable the different rooms when you enter/leave them. This was my original plan but I was still wondering how I can save all the data for different switches/in-game events/interactions, but using the save asset solved that.

    I saw almost all of the tutorials and posts you can find on the first few pages of google about saving data in unity. I first tried Unity's tutorial on persistent data (goes into saving with binary format). This worked for me however I didn't understand how to edit specific values in the save file as every time I saved a switches data, it would overwrite all the data inside the save file.

    TLDR: I was originally looking for some help with the logic behind saving data for interactive objects in the game. I tried out Unity's tutorial on saving using the binary format however I ran into an overwrite problem when saving different data. Now using a saving system found on the asset store to simplify things.
     
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    So we needed cross scene persistence on an item by item basis.

    And this is the general way we did.

    1) Serialization
    First things first is picking a serialization engine if we expect to save this to disk for saving/loading. We personally avoid the unity JsonUtility because it follows the same rules as the standard unity serializer which means no polymorphism.

    There are plenty of 3rd party serialization engines. We personally use the built in .Net serializer in System.Runtime.Serializer:
    https://msdn.microsoft.com/en-us/library/system.runtime.serialization(v=vs.110).aspx

    We used to use the old BinaryFormatter, but I would like to be able to easily read our save files, so I wrote a JsonFormatter that is compatible with the .net serializer:
    https://github.com/lordofduct/space...ree/master/SPSerialization/Serialization/Json

    You can use any library you want though. Json.Net is really nice.

    2) Persistent Data Container

    Now we need a place to store persistent data that can store state information across scenes and can be saved to disk.

    This is our implementation, it's basically just a dictionary of <string,object> pairs. Any object in the game world can place values into and pull values from this based on a string key. This string key should be unique (we will get to this later).

    Note this class also implements some other things that may not be familiar.

    ServiceScriptableObject, this is a service based singleton implementation I have in my library that just quickly implements this thing as a service that can be retrieved from anywhere by calling 'Serivces.Get<ScenarioPersistentData>'. You can implement it as a singleton in your own preferred way. Later in this post you will see my usage in action.

    ITokenizable, this is an interface that implies an object can be tokenized. A state representation can be made of the object and it can be easily restored to that state by passing the token back in. When I implement ITokenizable I always make sure said token can be serialized for saving to disk. This is what we'll be saving our game via (and why we needed a 3rd party serializer since everything is typed 'object').
    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5.  
    6. using com.spacepuppy;
    7. using com.spacepuppy.Dynamic;
    8. using com.spacepuppy.Utils;
    9.  
    10. namespace com.mansion
    11. {
    12.  
    13.     public class ScenarioPersistentData : ServiceScriptableObject<ScenarioPersistentData>, ITokenizable
    14.     {
    15.  
    16.         #region Fields
    17.      
    18.         private Dictionary<string, object> _table = new Dictionary<string, object>();
    19.  
    20.         #endregion
    21.  
    22.         #region IPersistentDataStore Interface
    23.      
    24.         public bool Contains(string id)
    25.         {
    26.             return _table.ContainsKey(id);
    27.         }
    28.  
    29.         public T GetData<T>(string id)
    30.         {
    31.             object result;
    32.             if (_table.TryGetValue(id, out result))
    33.             {
    34.                 if (result is T)
    35.                     return (T)result;
    36.                 else if (ConvertUtil.IsSupportedType(typeof(T)))
    37.                     return ConvertUtil.ToPrim<T>(result);
    38.                 else
    39.                 {
    40.                     try
    41.                     {
    42.                         return (T)result;
    43.                     }
    44.                     catch(System.Exception)
    45.                     {
    46.                         return default(T);
    47.                     }
    48.                 }
    49.             }
    50.             else
    51.             {
    52.                 throw new KeyNotFoundException();
    53.             }
    54.         }
    55.  
    56.         public bool TryGetData<T>(string id, out T data)
    57.         {
    58.             object result;
    59.             if (_table.TryGetValue(id, out result))
    60.             {
    61.                 if (result is T)
    62.                     data = (T)result;
    63.                 else if (ConvertUtil.IsSupportedType(typeof(T)))
    64.                     data = ConvertUtil.ToPrim<T>(result);
    65.                 else
    66.                 {
    67.                     try
    68.                     {
    69.                         data = (T)result;
    70.                     }
    71.                     catch (System.Exception)
    72.                     {
    73.                         data = default(T);
    74.                         return false;
    75.                     }
    76.                 }
    77.                 return true;
    78.             }
    79.             else
    80.             {
    81.                 data = default(T);
    82.                 return false;
    83.             }
    84.         }
    85.  
    86.         public void SetData<T>(string id, T data)
    87.         {
    88.             _table[id] = data;
    89.         }
    90.      
    91.         public bool Remove(string id)
    92.         {
    93.             return _table.Remove(id);
    94.         }
    95.  
    96.         public void Clear()
    97.         {
    98.             _table.Clear();
    99.         }
    100.  
    101.         #endregion
    102.  
    103.         #region ITokenizable Interface
    104.  
    105.         public object CreateStateToken()
    106.         {
    107.             var arr = new object[_table.Count];
    108.             var e = _table.GetEnumerator();
    109.             int i = 0;
    110.             while(e.MoveNext())
    111.             {
    112.                 if(e.Current.Value != null)
    113.                 {
    114.                     arr[i] = new ValuePair()
    115.                     {
    116.                         Id = e.Current.Key,
    117.                         Value = e.Current.Value
    118.                     };
    119.                 }
    120.                 else
    121.                 {
    122.                     arr[i] = e.Current.Key;
    123.                 }
    124.                 i++;
    125.             }
    126.             return arr;
    127.         }
    128.  
    129.         public void RestoreFromStateToken(object token)
    130.         {
    131.             _table.Clear();
    132.             var arr = token as object[];
    133.             if (arr != null)
    134.             {
    135.                 for(int i = 0; i < arr.Length; i++)
    136.                 {
    137.                     if(arr[i] is ValuePair)
    138.                     {
    139.                         var pair = (ValuePair)arr[i];
    140.                         _table[pair.Id] = pair.Value;
    141.                     }
    142.                     else if(arr[i] is string)
    143.                     {
    144.                         _table[arr[i] as string] = null;
    145.                     }
    146.                 }
    147.             }
    148.         }
    149.  
    150.         #endregion
    151.  
    152.         #region Special Types
    153.  
    154.         [System.Serializable]
    155.         private struct ValuePair
    156.         {
    157.             public string Id;
    158.             public object Value;
    159.         }
    160.      
    161.         #endregion
    162.  
    163.     }
    164.  
    165. }
    166.  
    3) Uniquely Identified Objects

    Next we need to be able to uniquely identify our objects.

    As we travel from scene to scene if we return how do we know what data in the ScenarioPersistentData container is ours? Well a unique id will help with this.

    Because our ScenarioPersistentData accepts a string, that id can really be anything. You can just have a 'string' field on your scripts that persist and put whatever you want in there. But this can be hard to keep track of over time... so a tool that allows easily generating unique id's would be nice.

    Personally I do this with a type I created called 'ShortUid':
    https://github.com/lordofduct/space...b/master/SpacepuppyUnityFramework/ShortUID.cs

    This type is very similar to a GUID, but only 64-bits in size and not globally unique. Instead it's project unique. I basically create a uid based on the current 'ticks' value from a DateTime. The odds you and a team mate click to create a suid at the same exact tick is near impossible... just don't generate a bunch of them at once through code.

    And of course we have an editor for it so that we can easily generate suid's in the inspector:
    https://github.com/lordofduct/space...rameworkEditor/Base/ShortUidPropertyDrawer.cs

    Now any script that needs to be uniquely identified can just have a ShortUid field on it like so:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.Scenario;
    7. using com.spacepuppy.Utils;
    8.  
    9. namespace com.mansion.Tools
    10. {
    11.  
    12.     [Infobox("If this object gets destroyed, any object in a future scene with the same 'Id' will also get destroyed and the 'OnStartedDirty' trigger called when it is first loaded. Otherwise OnStartedClean is called.", MessageType = InfoBoxMessageType.Info)]
    13.     public class PersistentConsumable : TriggerableMechanism
    14.     {
    15.      
    16.         #region Fields
    17.  
    18.         [SerializeField]
    19.         [ShortUid.Config(AllowZero = true)]
    20.         private ShortUid _id;
    21.  
    22.         [SerializeField]
    23.         private IEntity _entity;
    24.         [SerializeField]
    25.         private bool _autoDestroyEntity = true;
    26.  
    27.         [SerializeField]
    28.         [UnityEngine.Serialization.FormerlySerializedAs("_onDestroyedDueToPersistence")]
    29.         private Trigger _onStartedDirty;
    30.  
    31.         [SerializeField]
    32.         [UnityEngine.Serialization.FormerlySerializedAs("_onInitializedWithoutDestroy")]
    33.         private Trigger _onStartedClean;
    34.  
    35.         #endregion
    36.  
    37.         #region CONSTRUCTOR
    38.  
    39.         protected override void Start()
    40.         {
    41.             base.Start();
    42.  
    43.             var data = Services.Get<ScenarioPersistentData>();
    44.             object token;
    45.             if (_id != ShortUid.Zero && data != null && data.TryGetData<object>(_id.ToString(), out token))
    46.             {
    47.                 if (_onStartedDirty.Count > 0) _onStartedDirty.ActivateTrigger(this, null);
    48.                 if (_autoDestroyEntity && _entity != null) ObjUtil.SmartDestroy(_entity.gameObject);
    49.             }
    50.             else
    51.             {
    52.                 if (_onStartedClean.Count > 0) _onStartedClean.ActivateTrigger(this, null);
    53.             }
    54.         }
    55.  
    56.         #endregion
    57.  
    58.         #region Trigger Interface
    59.  
    60.         public override bool Trigger(object sender, object arg)
    61.         {
    62.             if (!this.CanTrigger) return false;
    63.             if (_id == ShortUid.Zero) return false;
    64.  
    65.             var data = Services.Get<ScenarioPersistentData>();
    66.             if (data == null) return false;
    67.          
    68.             data.SetData<object>(_id.ToString(), null);
    69.  
    70.             return false;
    71.         }
    72.  
    73.         #endregion
    74.      
    75.     }
    76.  
    77. }
    78.  
    Visual:
    PersistentConsumable01.png

    4) Now Do Stuff

    Now on different events you can do things.

    Such as that 'PersistentConsumable'. What it does is when signaled it caches it's Suid with a value of null, it does null because we don't really need any information with it. What it's signaling is that if the Suid exists in the persistence table that means this object has been consumed/used. So if the scene is ever reloaded to destroy itself so that it can't be picked up again.

    Note it uses my T&I system in the form of a 'Trigger'. This 'Trigger' is very similar to the 'UnityEvent' system, I just built it a couple years before Unity added theirs. We still use our system as it meets our needs much nicer. But you can just think of it as the same as 'UnityEvent'.

    Other things though might store more state information into the ScenarioPersistenData container.

    For example this is the script that saves the players state from scene to scene and game to game:
    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections.Generic;
    4.  
    5. using com.spacepuppy;
    6. using com.spacepuppy.Utils;
    7.  
    8. using com.mansion.Entities.Actors.Player;
    9. using com.mansion.Entities.Inventory;
    10. using com.mansion.Entities.Weapons;
    11.  
    12. using com.mansion.Messages;
    13.  
    14. namespace com.mansion.Scenarios
    15. {
    16.  
    17.     public class PlayerScenarioPersistentStateController : SPComponent, IScenarioStateChangedGlobalHandler
    18.     {
    19.  
    20.         public const string ID_HASH = "*playerstate";
    21.  
    22.         #region Fields
    23.  
    24.         [SerializeField]
    25.         [DefaultFromSelf(UseEntity = true)]
    26.         private HealthMeter _healthMeter;
    27.  
    28.         [SerializeField]
    29.         [DefaultFromSelf(UseEntity = true)]
    30.         private InventoryPouch _inventoryPouch;
    31.  
    32.         [SerializeField]
    33.         [DefaultFromSelf(UseEntity = true)]
    34.         private AmmoPouch _ammoPouch;
    35.  
    36.         [SerializeField]
    37.         [DefaultFromSelf(UseEntity = true)]
    38.         private PlayerWeaponPouch _weaponPouch;
    39.      
    40.         [SerializeField]
    41.         [DefaultFromSelf(UseEntity = true)]
    42.         private PlayerGrappledState _grappleState;
    43.  
    44.         [System.NonSerialized]
    45.         private PlayerEntity _entity;
    46.  
    47.         #endregion
    48.  
    49.         #region CONSTRUCTOR
    50.  
    51.         protected override void Start()
    52.         {
    53.             base.Start();
    54.  
    55.             _entity = PlayerEntity.PlayerPool.GetFromSource(this);
    56.             if (_entity != null)
    57.             {
    58.                 if (_healthMeter == null) _healthMeter = _entity.HealthMeter;
    59.                 if (_inventoryPouch == null) _inventoryPouch = _entity.FindComponent<InventoryPouch>();
    60.                 if (_ammoPouch == null) _ammoPouch = _entity.FindComponent<AmmoPouch>();
    61.                 if (_weaponPouch == null) _weaponPouch = _entity.FindComponent<PlayerWeaponPouch>();
    62.                 if (_grappleState == null) _grappleState = _entity.FindComponent<PlayerGrappledState>();
    63.             }
    64.  
    65.             this.DoLoad();
    66.         }
    67.  
    68.         protected override void OnEnable()
    69.         {
    70.             base.OnEnable();
    71.  
    72.             Messaging.RegisterGlobal<IScenarioStateChangedGlobalHandler>(this);
    73.         }
    74.  
    75.         protected override void OnDisable()
    76.         {
    77.             base.OnDisable();
    78.  
    79.             Messaging.UnregisterGlobal<IScenarioStateChangedGlobalHandler>(this);
    80.         }
    81.  
    82.         #endregion
    83.  
    84.         #region Properties
    85.      
    86.         #endregion
    87.  
    88.         #region Methods
    89.  
    90.         private void DoLoad()
    91.         {
    92.             if (_entity == null || _entity.Uid.Value == 0) return;
    93.  
    94.             var data = Services.Get<ScenarioPersistentData>();
    95.             if (data != null)
    96.             {
    97.                 PlayerStateToken token;
    98.                 if (!data.TryGetData<PlayerStateToken>(_entity.Uid.ToString() + ID_HASH, out token)) return;
    99.              
    100.                 if (_healthMeter != null)
    101.                 {
    102.                     _healthMeter.MaxHealth = token.MaxHealth;
    103.                     _healthMeter.Health = token.Health;
    104.                 }
    105.  
    106.                 if (_inventoryPouch != null)
    107.                 {
    108.                     var invtoken = token.Inventory;
    109.                     _inventoryPouch.RestoreFromStateToken(invtoken);
    110.                 }
    111.  
    112.                 if (_ammoPouch != null)
    113.                 {
    114.                     _ammoPouch.RestoreFromStateToken(token.AmmoToken);
    115.                 }
    116.              
    117.                 if (_grappleState != null)
    118.                 {
    119.                     _grappleState.enabled = !token.ImmuneToGrapple;
    120.                 }
    121.  
    122.  
    123.  
    124.                 //sync the weapon pouch after load
    125.                 if (_weaponPouch != null)
    126.                 {
    127.                     foreach (var w in _weaponPouch.Weapons)
    128.                     {
    129.                         var gun = w as GunWeapon;
    130.                         if (gun == null) continue;
    131.  
    132.                         var item = WeaponInventoryItem.GetFromWeapon(gun);
    133.                         if (item == null) continue;
    134.                      
    135.                         if (ConvertUtil.IsNumeric(item.SaveToken)) gun.AmmoInMagazine = ConvertUtil.ToInt(item.SaveToken);
    136.                     }
    137.  
    138.                     var weapItem = _inventoryPouch.Items.Find(token.CurrentEquippedWeaponItem) as WeaponInventoryItem;
    139.                     if(weapItem != null)
    140.                     {
    141.                         var weap = weapItem.GetRelatedWeaponOnPlayer(_weaponPouch);
    142.                         if(weap != null)
    143.                         {
    144.                             _weaponPouch.SetCurrentWeapon(weap);
    145.                         }
    146.                     }
    147.                 }
    148.             }
    149.         }
    150.  
    151.         private void DoSave(ScenarioPersistentData data)
    152.         {
    153.             if (_entity == null || _entity.Uid.Value == 0) return;
    154.  
    155.             //sync the weapon pouch before save
    156.             string equippedWeapon = null;
    157.             if (_weaponPouch != null)
    158.             {
    159.                 foreach(var w in _weaponPouch.Weapons)
    160.                 {
    161.                     var gun = w as GunWeapon;
    162.                     if (gun == null) continue;
    163.  
    164.                     var item = WeaponInventoryItem.GetFromWeapon(gun);
    165.                     if (item == null) continue;
    166.  
    167.                     item.SaveToken = gun.AmmoInMagazine;
    168.                 }
    169.  
    170.                 var weap = WeaponInventoryItem.GetFromWeapon(_weaponPouch.CurrentWeapon);
    171.                 if (weap != null) equippedWeapon = weap.name;
    172.             }
    173.  
    174.             //perform save
    175.             var token = new PlayerStateToken();
    176.             if (_healthMeter != null)
    177.             {
    178.                 token.Health = _healthMeter.Health;
    179.                 token.MaxHealth = _healthMeter.MaxHealth;
    180.             }
    181.  
    182.             if (_inventoryPouch != null)
    183.             {
    184.                 token.Inventory = _inventoryPouch.CreateStateToken();
    185.             }
    186.  
    187.             if (_ammoPouch != null)
    188.             {
    189.                 token.AmmoToken = _ammoPouch.CreateStateToken();
    190.             }
    191.          
    192.             if (_grappleState != null)
    193.             {
    194.                 token.ImmuneToGrapple = !_grappleState.enabled;
    195.             }
    196.  
    197.             token.CurrentEquippedWeaponItem = equippedWeapon;
    198.  
    199.             data.SetData<PlayerStateToken>(_entity.Uid.ToString() + ID_HASH, token);
    200.         }
    201.  
    202.         #endregion
    203.  
    204.         #region IScenarioChangedGlobalHandler Interface
    205.  
    206.         void IScenarioStateChangedGlobalHandler.OnLoad(ScenarioPersistentData container, LoadReason reason)
    207.         {
    208.             //let start deal with this
    209.         }
    210.  
    211.         void IScenarioStateChangedGlobalHandler.OnSave(ScenarioPersistentData data, SaveReason reason)
    212.         {
    213.             this.DoSave(data);
    214.         }
    215.  
    216.         #endregion
    217.  
    218.  
    219.         #region Special Types
    220.  
    221.  
    222.         [System.Serializable]
    223.         private struct PlayerStateToken
    224.         {
    225.             public float Health;
    226.             public float MaxHealth;
    227.             public object Inventory;
    228.             public object AmmoToken;
    229.             public bool ImmuneToGrapple;
    230.             public string CurrentEquippedWeaponItem;
    231.         }
    232.  
    233.         #endregion
    234.  
    235.     }
    236.  
    237. }
    238.  
    When we save the game we just tokenize the ScenarioPersistentData container and stick it in our save token:
    Code (csharp):
    1.  
    2.         public override void SaveScenario(SaveReason reason, bool saveToDisk)
    3.         {
    4.             var data = Services.Get<ScenarioPersistentData>();
    5.             if (data != null)
    6.             {
    7.                 this.OnSavePersistentData(data, reason);
    8.                 Messaging.Broadcast<IScenarioStateChangedGlobalHandler>((o) => o.OnSave(data, reason));
    9.                 _stateToken = data.CreateStateToken();
    10.             }
    11.  
    12.             if(saveToDisk)
    13.             {
    14.                 var token = new SaveGameToken()
    15.                 {
    16.                     Data = this.StateToken,
    17.                     Checkpoint = this.Checkpoint
    18.                 };
    19.  
    20.                 token.Save(System.IO.Path.Combine(Application.persistentDataPath, this.GetDefaultSaveFileName()));
    21.  
    22.                 var stats = Services.Get<com.mansion.Statistics.GameStatistics>();
    23.                 if (stats != null) stats.AdjustStat(com.mansion.Statistics.GameStatistics.STAT_SAVEDGAME, 1f);
    24.             }
    25.         }
    26.  
    The save token gets both the persistent data, as well as the checkpoint (other stuff could be tacked on as well). You may notice I displatch a message before saving called 'IScenarioStateChangedGlobalHandler.OnSave', this is to signal to anything in the scene that we're saving and to store any state information if it needs to before saving. This way adding new stuff to the save chain is as simple as implementing that interface (like PlayerScenarioPersistentStateController does).

    The resulting save file looks a bit like this:
    Code (csharp):
    1.  
    2. {
    3.     "@type" : "com.mansion.SaveGameToken",
    4.     "Data" : [
    5.         "System.Object[]",
    6.         "08D4C8E471CF4811",
    7.         "08D5E0527E69251B",
    8.         {
    9.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    10.             "Id" : "*Ep1*GameState*",
    11.             "Value" : {
    12.                 "@type" : "com.mansion.Scenarios.Episode1.Episode1Controller+Token",
    13.                 "ObjectivesToken" : [
    14.                     "com.mansion.Objective[]",
    15.                     {
    16.                         "@type" : "com.mansion.Objective",
    17.                         "Message" : "Explore the mansion."
    18.                     }
    19.                 ],
    20.                 "Statistics" : {
    21.                     "@type" : "com.mansion.Statistics.Ledger",
    22.                     "RoomEntered" : 10,
    23.                     "SavedGame" : 1,
    24.                     "RoundsFired" : 6,
    25.                     "MobsKilled" : 1,
    26.                     "EP1_KILLALLKITTIES" : 1
    27.                 },
    28.                 "CurrentTime" : {
    29.                     "@type" : "System.TimeSpan",
    30.                     "_ticks" : 807060000
    31.                 },
    32.                 "MobsKilled" : 1,
    33.                 "MobsSpawned" : null,
    34.                 "DocumentsScore" : null,
    35.                 "Difficulty" : 2,
    36.                 "Inventory" : null
    37.             }
    38.         },
    39.         {
    40.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    41.             "Id" : "08D52C2C847D35E2",
    42.             "Value" : {
    43.                 "@type" : "com.mansion.Cutscenes.CutsceneSequenceManager+Token",
    44.                 "SequenceIndex" : 0
    45.             }
    46.         },
    47.         {
    48.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    49.             "Id" : "08D52C1057286A0C",
    50.             "Value" : {
    51.                 "@type" : "com.mansion.Cutscenes.CutsceneSequenceManager+Token",
    52.                 "SequenceIndex" : 7
    53.             }
    54.         },
    55.         {
    56.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    57.             "Id" : "08D5DAD6433EACF3",
    58.             "Value" : -1
    59.         },
    60.         {
    61.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    62.             "Id" : "08D5DADB90606449",
    63.             "Value" : -1
    64.         },
    65.         {
    66.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    67.             "Id" : "08D5DAD63E450A06",
    68.             "Value" : -1
    69.         },
    70.         {
    71.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    72.             "Id" : "08D52B5BC2171FCC*playerstate",
    73.             "Value" : {
    74.                 "@type" : "com.mansion.Scenarios.PlayerScenarioPersistentStateController+PlayerStateToken",
    75.                 "Health" : 100,
    76.                 "MaxHealth" : 100,
    77.                 "Inventory" : [
    78.                     "System.Object[]",
    79.                     {
    80.                         "@type" : "com.mansion.Entities.Inventory.InventoryPouch+Token",
    81.                         "name" : "Ep1_Gun",
    82.                         "token" : 8
    83.                     },
    84.                     "KEY - Library Key"
    85.                 ],
    86.                 "AmmoToken" : [
    87.                     "com.mansion.Entities.Weapons.AmmoPouch+ValuePair[]",
    88.                     {
    89.                         "@type" : "com.mansion.Entities.Weapons.AmmoPouch+ValuePair",
    90.                         "Type" : 0,
    91.                         "Quantity" : 21
    92.                     }
    93.                 ],
    94.                 "ImmuneToGrapple" : false,
    95.                 "CurrentEquippedWeaponItem" : "Ep1_Gun"
    96.             }
    97.         },
    98.         {
    99.             "@type" : "com.mansion.ScenarioPersistentData+ValuePair",
    100.             "Id" : "*SceneVariables",
    101.             "Value" : {
    102.                 "@type" : "com.spacepuppy.Dynamic.StateToken",
    103.                 "ThreatLevel" : 1,
    104.                 "PlayerCorpseInspections" : 0,
    105.                 "JustEnteredNewRoom" : false
    106.             }
    107.         },
    108.         "08D4C7548B21692A",
    109.         "08D5B05043538B26",
    110.         "08D5B04C45FA6487"
    111.     ],
    112.     "Checkpoint" : {
    113.         "@type" : "com.mansion.CheckpointState",
    114.         "Scene" : "Mansion_A",
    115.         "Position" : {
    116.             "@type" : "UnityEngine.Vector3",
    117.             "x" : 1.64429235458374,
    118.             "y" : 0.0400000102818012,
    119.             "z" : 13.2476835250854
    120.         },
    121.         "Rotation" : {
    122.             "@type" : "UnityEngine.Quaternion",
    123.             "x" : 0,
    124.             "y" : 0.115706734359264,
    125.             "z" : 0,
    126.             "w" : 0.993283450603485
    127.         }
    128.     }
    129. }
    130.  
     
    Last edited: Jul 8, 2018
    CHEMAX3X, LMan, eses and 1 other person like this.
  5. Unfurl_Games

    Unfurl_Games

    Joined:
    Jan 4, 2014
    Posts:
    16
    Damn, thank you lordofduct for the insane detail you went into here. This will be extremely helpful for people like me to keep a note of when doing any sort of saving data in this style. I'll be sure to keep a close eye on this while I'm hammering away at the save system. Seems like a really useful way to keep track of data for many different objects.

    I'll have to see what happens when I get back to the save system! Hoping people find this thread to help answer some questions about persistent data for interactable objects like items or switches. Most tutorials walk through saving player data and it's difficult to try to understand saving data from many different scripts.
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,378
    Yeah, a lot of tutorials out there really just deal with the basics of "how to serialize", and even then don't go into the limitations of the serializer they often end up using.

    One day I should write a proper tut about it. But then again, no idea where I'd post it (I no longer maintain my blog... haven't in years).

    Ain't gonna be this week though, our Steam release is on the 13th. . .
    https://store.steampowered.com/app/868570/Prototype_Mansion__Used_No_Cover/
     
  7. Unfurl_Games

    Unfurl_Games

    Joined:
    Jan 4, 2014
    Posts:
    16
    Yep, most tutorials are very simple and don't really use real world examples. If you do end up writing a tutorial on this similar to what you posted above, I'm sure tons of people will use it.

    Your game looks great, I wish the best of luck to your launch (i'll be keeping my eye on it :) )!
     
  8. neoshaman

    neoshaman

    Joined:
    Feb 11, 2011
    Posts:
    6,466
    eses likes this.