Search Unity

How to manage a simple saving/loading system

Discussion in 'Scripting' started by Flynn_Prime, Jun 20, 2018.

  1. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    387
    I am creating a dungeon crawler for mobile devices. I'm just starting to save small amounts of data in my game. I only need to save trivial data such as players inventory, stats and level data, achievements etc. So far, what I have works but I am after some advice/ coding help before I carry on extending this system as I am unsure of the correct/normal route to take.

    Code (CSharp):
    1. using System;
    2.  
    3. [Serializable]
    4. public class GameData
    5. {
    6.     public string characterName;
    7.     public int characterLevel;
    8.  
    9.     public float intelligence;
    10.     public float wisdom;
    11.     public float agility;
    12.     public float vitality;
    13.  
    14.     public GameData(string name, int level, float bInt, float bWis, float bAgi, float bVit)
    15.     {
    16.         characterName = name;
    17.         characterLevel = level;
    18.         intelligence = bInt;
    19.         wisdom = bWis;
    20.         agility = bAgi;
    21.         vitality = bVit;
    22.     }
    23. }

    Code (CSharp):
    1. public class SavePlayerData : MonoBehaviour
    2. {
    3.     private Character player;
    4.  
    5.     private void Awake()
    6.     {
    7.         player = GetComponent<Character>();
    8.     }
    9.  
    10.     public void SaveFile()
    11.     {
    12.         string destination = Application.persistentDataPath + "/Save.dat";
    13.         FileStream file;
    14.  
    15.         if (File.Exists(destination))
    16.             file = File.OpenWrite(destination);
    17.         else file = File.Create(destination);
    18.  
    19.         GameData data = new GameData(player.characterName, player.characterLevel, player.Intelligence.BaseValue, player.Wisdom.BaseValue, player.Agility.BaseValue, player.Vitality.BaseValue);
    20.         BinaryFormatter bf = new BinaryFormatter();
    21.         bf.Serialize(file, data);
    22.         file.Close();
    23.     }
    24.  
    25.     public void LoadFile()
    26.     {
    27.         string destination = Application.persistentDataPath + "/Save.dat";
    28.         FileStream file;
    29.  
    30.         if (File.Exists(destination))
    31.             file = File.OpenRead(destination);
    32.         else
    33.             return;
    34.  
    35.         BinaryFormatter bf = new BinaryFormatter();
    36.         GameData data = (GameData)bf.Deserialize(file);
    37.         file.Close();
    38.  
    39.         player.characterName = data.characterName;
    40.         player.characterLevel = data.characterLevel;
    41.         player.Intelligence.BaseValue = data.intelligence;
    42.         player.Wisdom.BaseValue = data.wisdom;
    43.         player.Agility.BaseValue = data.agility;
    44.         player.Vitality.BaseValue = data.vitality;
    45.     }
    46. }

    I would like some advice on how I should handle different types of saved data. For example, do I have a single big save file? If so, my biggest question is where/how on earth would I call the data constructor as I would need to be saving data from multiple classes?

    Or is it better to use multiple save files for a single purpose? I.e. A save file for stats, one for inventory, one for achievements etc. and have multiple constructors in Game Data that save different data to different files?

    If you could please support any advice with a couple of pros and cons of what you suggest.

    Any input would be very much appreciated!

    Chris
     
  2. dgoyette

    dgoyette

    Joined:
    Jul 1, 2016
    Posts:
    4,195
    I don't think there's any real value to splitting things into multiple files. I'd say you can keep your GameData class as the "root" of your save data, and create other classes that store the values for things like inventory or achievements. Then your GameData class could have a property like `public AchievementData AchievementData`. As long as all the classes are serializable, that should serialize into a single file just fine.

    As for how you get the data out of your game and into your GameData classes, that's up to you. You could write one static method somewhere in your code that takes all of the dependencies as parameters. You could add methods to the different MonoBehaviour classes, such as "GetGameData", which returns a GameData (or an AchievementData object, etc) based on the current state of the object. Kind of up to you.
     
  3. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    387
    Thanks for the reply. How can I write multiple times to the same file, from different script, without erasing the data already stored in the file? For example, if I saved my level data in Save.dat but then called the save function from my achievement script. How would I ensure the achievement data is added to the save file and does not overwrite? Sorry if this is a trivial question. I'm very new to serialization.

    EDIT: I now realise this is a stupid question. Please ignore :D
     
    Last edited: Jun 21, 2018
  4. TimmyTheTerrible

    TimmyTheTerrible

    Joined:
    Feb 18, 2017
    Posts:
    186
    I'm no expert on this subject, there are probably much better ways to do it, but I can share one of my noobish approaches. I made an interface called ISaveable, and a singleton class GameManager that did the saving/loading. When it came time to save/load, the game manager would use GetComponent<ISaveable>() on all objects in currently loaded scenes. The game manager singleton had functions called SaveBool, SaveFloat, SaveInteger, etc. Basically it stored these primitive types in lists (organized by scene).

    So it basically went, Game manager calls SaveScene.

    SaveScene function calls get component on all objects and their children to check if they implement ISaveable.

    If they do, call there Save function implementations, which basically adds all of there data to the Game manager lists of bools, floats, strings, etc.

    Then finally, at the end of the SaveScene function, write these lists to the file, and then clear them.
     
  5. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    387
    I've steered away from singletons up until now, but I like this approach. Thanks for taking the time to reply!
     
  6. WheresMommy

    WheresMommy

    Joined:
    Oct 4, 2012
    Posts:
    890
    Did you think of JSON .Net for Unity or JSON Utility build in Unity? Look it up, this might be, what you want. Working with JSON and replacing parts or updating is quite easy.
     
  7. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    387
    I have used JSON.Net before but I think I prefer using this way. Although from what I remember JSON.Net was very flexible and also well documented so I may have to delve into it again. I was even more of a newbie back then
     
  8. GeorgeCH

    GeorgeCH

    Joined:
    Oct 5, 2016
    Posts:
    222
    I'd also recommend JSON, not least because it's easily editable (read: debuggable and handy for testing) and also because it paves the way for you to eventually turn the system into cloud saves (instead of storing everything on the device, you store everything in the cloud and then save/load data from there as necessary).

    I also second earlier advice regarding interfaces: what I do is roughly like this:
    1. Download the master JSON save file from the cloud
    2. Store its contents as a public variable in my singleton class
    3. Start the WaitUntilSavedDataIsLoaded() coroutine in the singleton class, which basically pauses all further game initialization until the contents of each class that implements the ILoadable interface have been, well, loaded
    4. Start the WaitUntilDataIsLoaded() coroutine in all classes that implement the ILoadable interface (which are classes that have things that need to be saved/loaded)
    5. Wait for the coroutine to finish doing what it does - which is for each implementing class to dig into the master JSON file in the game controller singleton, retrieve class-specific save data, and do whatever it needs to do with it
    6. Set the value of the isLoaded bool to true
    7. Once all classes implementing the ILoadable interface have set their isLoaded bool to true, unpause the game loading process

    Here is an example of the interface:
    Code (CSharp):
    1. public interface ILoadable
    2. {
    3.  
    4.     bool isLoaded { get; set; }
    5.     void LoadCloudData();
    6.  
    7.     IEnumerator WaitForFirebaseRemoteConfig();
    8. }
     
    Flynn_Prime likes this.