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
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice

Saving from multiple script

Discussion in 'Scripting' started by baazul, Aug 6, 2019.

  1. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    Hello,

    I followed this tutorial:


    And it is working very well so far. Here is my current code:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class PlayerData
    7. {
    8.     public int test;
    9.  
    10.     public PlayerData (Stats stats)
    11.     {
    12.         test= stats.test;
    13.     }
    14. }
    15.  
    Code (CSharp):
    1. using UnityEngine;
    2. using System.IO;
    3. using System.Runtime.Serialization.Formatters.Binary;
    4.  
    5. public static class SaveSystem
    6. {
    7.     public static void SavePlayer (Stats stats)
    8.     {
    9.         BinaryFormatter formatter = new BinaryFormatter();
    10.         string path = Application.persistentDataPath + "/player.fun";
    11.         FileStream stream = new FileStream(path, FileMode.Create);
    12.  
    13.         PlayerData data = new PlayerData(stats);
    14.  
    15.         formatter.Serialize(stream, data);
    16.         stream.Close();
    17.     }
    18.  
    19.     public static PlayerData LoadPlayer()
    20.     {
    21.         string path = Application.persistentDataPath + "/player.fun";
    22.         if (File.Exists(path))
    23.         {
    24.             BinaryFormatter formatter = new BinaryFormatter();
    25.             FileStream stream = new FileStream(path, FileMode.Open);
    26.  
    27.             PlayerData data = formatter.Deserialize(stream) as PlayerData;
    28.             stream.Close();
    29.  
    30.             return data;
    31.         }
    32.  
    33.         else
    34.         {
    35.             Debug.LogError("Save file not found in " + path);
    36.             return null;
    37.         }
    38.     }
    39. }
    40.  
    And inside my class Stats:

    Code (CSharp):
    1.     public void SavePlayer()
    2.     {
    3.         SaveSystem.SavePlayer(this);
    4.     }
    5.  
    6.     public void LoadPlayer()
    7.     {
    8.         PlayerData data = SaveSystem.LoadPlayer();
    9.  
    10.         test= data.test;
    11.     }
    12.  
    13. }
    So I can save value from the "stats" script now but in fact I have values inside another script called "Combat" that I would like to save as well. I tried various things to make it happen, without any success. If it is possible, how would you save data from this second script please ?

    Regards,
     
    honor0102 likes this.
  2. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I am thinking of 2 solutions currently. Both being probably bad but I would expect them to be working anyway

    1. Merge all my scripts into one to have all the data I want to save in a single script
    2. Make multiple save systems for each script and call them all on the same button :/

    Any better idea or tutorial I could follow somewhere ? :p
     
  3. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    You can reference one class from another and it will "nest" them. You can have a GameData class (which would be the one you actually use in SaveSystem), and within GameData you have a public PlayerData, Combat, and anything else you need to save.
     
    baazul likes this.
  4. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I may have misunderstood your solution or maybe missed the correct way to apply it. Here what I tried:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class PlayerData
    7. {
    8.     public int test;
    9.     public int sword;
    10.     public Combat combat;
    11.  
    12.     public PlayerData (Stats stats)
    13.     {
    14.  
    15.         test= stats.test;
    16.         sword = combat.sword;
    17.     }
    18. }
    19.  
    And inside the "Stats" class

    Code (CSharp):
    1.     public void SavePlayer()
    2.     {
    3.         SaveSystem.SavePlayer(this);
    4.     }
    5.     public void LoadPlayer()
    6.     {
    7.         PlayerData data = SaveSystem.LoadPlayer();
    8.         test= data.test;
    9.         combat.sword = data.sword;
    10.     }
    When I click on save it gives this error about the line "sword = combat.sword;" inside the PlayerData class:

    NullReferenceException: Object reference not set to an instance of an object
    PlayerData..ctor (Stats stats) (at Assets/Scripts/PlayerData.cs:44)
     
    Last edited: Aug 8, 2019
  5. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I made a lot of various test at random but did not find a solution yet :/
     
  6. ThySpektre

    ThySpektre

    Joined:
    Mar 15, 2016
    Posts:
    362
    This is a common limitation of Unity's component architecture.

    You can use some form of a manager class that unfortunately needs to know the details of where you data resides so you can access it and save/load and restore the data to the proper components.

    Also, as a suggestion depending on the size of your saved data, the .NET serialization routines are quite slow. If your data is simple, you may do much better both in file size and execution time if you serialize the data yourself.
     
    baazul likes this.
  7. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I tried this:

    SaveSystem:

    Code (CSharp):
    1. using UnityEngine;
    2. using System.IO;
    3. using System.Runtime.Serialization.Formatters.Binary;
    4.  
    5. public static class SaveSystem
    6. {
    7.     public static void SavePlayer (GameData gamedata)
    8.     {
    9.         BinaryFormatter formatter = new BinaryFormatter();
    10.         string path = Application.persistentDataPath + "/save1.dat";
    11.         FileStream stream = new FileStream(path, FileMode.Create);
    12.  
    13.         PlayerData data = new PlayerData(gamedata);
    14.  
    15.         formatter.Serialize(stream, data);
    16.         stream.Close();
    17.     }
    18.  
    19.     public static PlayerData LoadPlayer()
    20.     {
    21.         string path = Application.persistentDataPath + "/save1.dat";
    22.         if (File.Exists(path))
    23.         {
    24.             BinaryFormatter formatter = new BinaryFormatter();
    25.             FileStream stream = new FileStream(path, FileMode.Open);
    26.  
    27.             PlayerData data = formatter.Deserialize(stream) as PlayerData;
    28.             stream.Close();
    29.  
    30.             return data;
    31.         }
    32.  
    33.         else
    34.         {
    35.             Debug.LogError("Save file not found in " + path);
    36.             return null;
    37.         }
    38.     }
    39. }

    PlayerData:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class PlayerData
    7. {
    8.     public int level;
    9.  
    10.  
    11.     public Stats stats;
    12.  
    13.  
    14.  
    15.  
    16.  
    17.  
    18. public PlayerData (GameData gameData)
    19.     {
    20.         level = stats.level;
    21.  
    22.  
    23.  
    24.     }
    25. }
    26.  

    GameData:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class GameData : MonoBehaviour
    6. {
    7.     public int level;
    8.  
    9.     public Stats stats;
    10.     public PlayerData playerdata;
    11.  
    12.  
    13.     public void SavePlayer()
    14.     {
    15.         SaveSystem.SavePlayer(this);
    16.     }
    17.  
    18.     public void LoadPlayer()
    19.     {
    20.         PlayerData data = SaveSystem.LoadPlayer();
    21.  
    22.         level = data.level;
    23.  
    24.  
    25.     }
    26. }
    27.  
    I apparently just turned things around just to get the exact same result as my previous error. So it currently says:

    NullReferenceException: Object reference not set to an instance of an object
    PlayerData..ctor (GameData gameData) (at Assets/Scripts/PlayerData.cs:34)

    Which leads to the following line in visual studio when I double click it:

    level = stats.level;
     
    Last edited: Aug 9, 2019
  8. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    The problem is that, as it says, "stats" is null. You haven't assigned it anything and the constructor is the very first thing that is executed on that object, so "stats" is always going to be null when that line is executed.

    At some point you'd need to do "stats = new Stats();" or something in the constructor, though to be honest I'm not sure I see the purpose in passing around this "level" variable from one place to another at all, which your classes all seem to do. Or perhaps you meant for that line to be "level = gameData.level", since otherwise why are you passing in gameData to the constructor?
     
    Ryiah and baazul like this.
  9. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I have no clue why I did it this way myself, or almost. I am trying things half randomly and get happy whent it works. I am a beginner and while I was able to follow the video tutorial I linked. I don't understand most part of it which prevents me from adapting it to my needs.

    My needs are:

    - I have data in script A and I can save it
    - I also have data in script B that I want to save as well, using the video in the tutorial but I don't know how to adapt the code

    I tried few things with the "stats = new Stats();" you suggested, without success so far. I am not sure where to put it or if it needs something else to work
     
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,744
    OK, let me take a crack at this, and see if I can help fix the structure this entirely as opposed to picking at one error.

    You'll need to have one "master" class that holds everything. That doesn't mean it has its own copy of everything, but it needs to have reference to everything. I'm going to write this assuming that that class is GameData, which has reference to Stats and PlayerData. That is the class you need to serialize to the file (you're currently serializing PlayerData). Note that this master class should not be a MonoBehaviour.

    Try not to include redundant data in your structure, as it'll be hard to keep it all in sync without errors. (You have "level" and "stats" in multiple places.)

    There are really four things you need to do with these classes:
    1) Create the data based on what's going on in the game
    2) Serialize
    3) Deserialize
    4) Apply their data to the game after you load it

    You've got 2 and 3 handled, aside from the need to switch it to the GameData class that holds everything.

    I don't see anywhere where you're doing steps 1 or 4, and I think 1 is where you're mainly having issues crop up. GameData will need to be fed whatever objects have a state that needs to be saved, and shuffle the data from those objects into the appropriate classes (like PlayerData). This will probably be done in GameData's constructor. (Note: constructors don't get run when you deserialize an object).

    The constructor might look like:
    Code (csharp):
    1. public GameData(int level, PlayerGameObject player) {
    2. this.level = level;
    3. this.playerdata = new PlayerData(player);
    4. }
    PlayerData would have its own constructor that does similar things.

    Does this help?
     
    baazul likes this.
  11. PrieDayThor

    PrieDayThor

    Joined:
    Jun 18, 2015
    Posts:
    16
    Hey :)

    you could try an approach with a global save manager class, which stores instances of an interface. I'm not that good at explaining in english but a implementation would look something like this:

    The interface:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public interface ISaveAndLoadData
    6. {
    7.     void SaveData();
    8.  
    9.     void LoadData();
    10. }
    The savemanager:

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class SaveManager : MonoBehaviour
    7. {
    8.     private static SaveManager instance;
    9.  
    10.     public static SaveManager Instance => instance;
    11.  
    12.     private List<ISaveAndLoadData> _savers;
    13.  
    14.     // Create singleton if needed
    15.     private void Awake()
    16.     {
    17.         if(instance != null)
    18.         {
    19.             Destroy(gameObject);
    20.         }
    21.         else
    22.         {
    23.             instance = this;
    24.             _savers = new List<ISaveAndLoadData>();
    25.         }
    26.     }
    27.  
    28.     // Add a new saving script
    29.     public void AddSaver(ISaveAndLoadData saver)
    30.     {
    31.         if (_savers.Contains(saver))
    32.         {
    33.             return;
    34.         }
    35.         _savers.Add(saver);
    36.     }
    37.  
    38.     // Saves data on all scripts
    39.     public void TriggerGlobalSaved()
    40.     {
    41.         foreach(ISaveAndLoadData s in _savers)
    42.         {
    43.             s.SaveData();
    44.         }
    45.     }
    46.  
    47.     // Loads data on all scripts
    48.     public void TriggerGlobalLoad()
    49.     {
    50.         foreach (ISaveAndLoadData s in _savers)
    51.         {
    52.             s.LoadData();
    53.         }
    54.     }
    55. }
    An example class:

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5.  
    6. public class ExampleSavingClass : MonoBehaviour , ISaveAndLoadData
    7. {
    8.     // Use start to make sure that the save manager has initialized
    9.     private void Start()
    10.     {
    11.         SaveManager.Instance.AddSaver(this);
    12.     }
    13.  
    14.     public void LoadData()
    15.     {
    16.         // Your implementation for loading data aka. Brackeys approach
    17.         throw new System.NotImplementedException();
    18.     }
    19.  
    20.     public void SaveData()
    21.     {
    22.         // Your implementation for saving data aka. Brackeys approach
    23.     }
    24. }
    Of course you could add different layers of complexity :)
     
    baazul likes this.
  12. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    Yes it helps and I am grateful for the help you provided already. Also, I may have lacked clarity in my explanation. The GameData class was just one of my many attempts to make it works and it failed so far. So your answer being based on it is a bit confusing to me. I am going to copy my current code exactly as it is because it will be more relevant.

    I think this part does 1 (but it allows me to take data only from the script containing the "class Stats")

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [System.Serializable]
    6. public class PlayerData
    7. {
    8.     public int level;
    9.     public float currentHealth;
    10.     public float experience;
    11.     public float experienceToNextLevel;
    12.     public float MaximumHealth;
    13.     public int Strength;
    14.     public int tempStrenght;
    15.     public int Agility;
    16.     public int tempAgility;
    17.     public int Charisma;
    18.     public int tempCharisma;
    19.     public int TempStatsPoints;
    20.     public int sword;
    21.     public int rope;
    22.     public int axe;
    23.     public int torch;
    24.  
    25.  
    26.  
    27.  
    28.  
    29.  
    30.  
    31. public PlayerData (Stats stats)
    32.     {
    33.         level = stats.level;
    34.         currentHealth = stats.currentHealth;
    35.         experience = stats.experience;
    36.         experienceToNextLevel = stats.experienceToNextLevel;
    37.         MaximumHealth = stats.MaximumHealth;
    38.         Strength = stats.Strength;
    39.         tempStrenght = stats.tempStrenght;
    40.         Agility = stats.Agility;
    41.         tempAgility = stats.tempAgility;
    42.         Charisma = stats.Charisma;
    43.         tempCharisma = stats.tempCharisma;
    44.         TempStatsPoints = stats.TempStatsPoints;
    45.  
    46.  
    47.     }
    48. }
    I think this part does 2 and 3

    Code (CSharp):
    1. using UnityEngine;
    2. using System.IO;
    3. using System.Runtime.Serialization.Formatters.Binary;
    4.  
    5. public static class SaveSystem
    6. {
    7.     public static void SavePlayer (Stats stats)
    8.     {
    9.         BinaryFormatter formatter = new BinaryFormatter();
    10.         string path = Application.persistentDataPath + "/save1.dat";
    11.         FileStream stream = new FileStream(path, FileMode.Create);
    12.  
    13.         PlayerData data = new PlayerData(stats);
    14.  
    15.         formatter.Serialize(stream, data);
    16.         stream.Close();
    17.     }
    18.  
    19.     public static PlayerData LoadPlayer()
    20.     {
    21.         string path = Application.persistentDataPath + "/save1.dat";
    22.         if (File.Exists(path))
    23.         {
    24.             BinaryFormatter formatter = new BinaryFormatter();
    25.             FileStream stream = new FileStream(path, FileMode.Open);
    26.  
    27.             PlayerData data = formatter.Deserialize(stream) as PlayerData;
    28.             stream.Close();
    29.  
    30.             return data;
    31.         }
    32.  
    33.         else
    34.         {
    35.             Debug.LogError("Save file not found in " + path);
    36.             return null;
    37.         }
    38.     }
    39. }
    40.  

    And inside another class called Stats, a part of the code is probably doing 4:
    Code (CSharp):
    1.     public void SavePlayer()
    2.     {
    3.         SaveSystem.SavePlayer(this);
    4.     }
    5.  
    6.     public void LoadPlayer()
    7.     {
    8.         PlayerData data = SaveSystem.LoadPlayer();
    9.  
    10.         level = data.level;
    11.         currentHealth = data.currentHealth;
    12.         experience = data.experience;
    13.         experienceToNextLevel = data.experienceToNextLevel;
    14.         MaximumHealth = data.MaximumHealth;
    15.         Strength = data.Strength;
    16.         tempStrenght = data.tempStrenght;
    17.         Agility = data.Agility;
    18.         tempAgility = data.tempAgility;
    19.         Charisma = data.Charisma;
    20.         tempCharisma = data.tempCharisma;
    21.         TempStatsPoints = data.TempStatsPoints;
    22.  
    23.      
    24.     }
    This code is working. However, I have another class called "Combat" that contains data like the remaining life of the current enemy (enemyLife) and I don't know how to take data from this class simultaneously with the class "Stats"
     
    Last edited: Aug 9, 2019
    honor0102 likes this.
  13. baazul

    baazul

    Joined:
    Jul 29, 2018
    Posts:
    42
    I found a solution. Probably not the best practice but it works so far. I just made as much save script (and save file) as I need. So it will save data to 4 different files and load from each as well
     
    ThySpektre likes this.
  14. ThySpektre

    ThySpektre

    Joined:
    Mar 15, 2016
    Posts:
    362
    This is what the Unity experts suggested to me. I did not find it a clean solution either.
     
    baazul likes this.
  15. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    Wait... are you referring to the lengthy thread we had.

    No... that's not we suggested. lol
     
  16. ThySpektre

    ThySpektre

    Joined:
    Mar 15, 2016
    Posts:
    362
    Yes. A consequence of your suggested kludge.
     
  17. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,380
    What a weird necro.

    The suggestion of the thread wasn't to make 4 separate files. That was something baazul said after the lengthy thread. You quoted that, and I was saying "but that's not what we (as in this thread) had suggested". Cause it's not. The OP video may elude to expanding to that (though it doesn't explicitly say that and is up to your own interpretation)...

    But the people in this thread, like StarManta, aren't necessarily saying to do that. That's why when bazuul said they "found a solution", it was a way not yet touched on, bazuul found their own solution which is the thing you're saying "we" suggested. It's not. (and hey cool, if it worked for bazuul, it worked. Who are we to tell them to change something that works for them. They never came back after that.)

    And that was in 2019.

    You then necroed to misplace who made that suggestion again???

    o_O?
     
    Last edited: Jan 8, 2022
    Lurking-Ninja, _geo__ and Kurt-Dekker like this.
  18. ThySpektre

    ThySpektre

    Joined:
    Mar 15, 2016
    Posts:
    362
    It was indeed what was suggested in the thread I was referencing..
     
  19. _geo__

    _geo__

    Joined:
    Feb 26, 2014
    Posts:
    1,116