Saving from multiple script

Hello,

I followed this tutorial:

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

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerData
{
    public int test;

    public PlayerData (Stats stats)
    {
        test= stats.test;
    }
}
using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class SaveSystem
{
    public static void SavePlayer (Stats stats)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        string path = Application.persistentDataPath + "/player.fun";
        FileStream stream = new FileStream(path, FileMode.Create);

        PlayerData data = new PlayerData(stats);

        formatter.Serialize(stream, data);
        stream.Close();
    }

    public static PlayerData LoadPlayer()
    {
        string path = Application.persistentDataPath + "/player.fun";
        if (File.Exists(path))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            FileStream stream = new FileStream(path, FileMode.Open);

            PlayerData data = formatter.Deserialize(stream) as PlayerData;
            stream.Close();

            return data;
        }

        else
        {
            Debug.LogError("Save file not found in " + path);
            return null;
        }
    }
}

And inside my class Stats:

    public void SavePlayer()
    {
        SaveSystem.SavePlayer(this);
    }

    public void LoadPlayer()
    {
        PlayerData data = SaveSystem.LoadPlayer();

        test= data.test;
    }

}

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,

1 Like

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 :confused:

Any better idea or tutorial I could follow somewhere ? :stuck_out_tongue:

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.

1 Like

I may have misunderstood your solution or maybe missed the correct way to apply it. Here what I tried:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerData
{
    public int test;
    public int sword;
    public Combat combat;

    public PlayerData (Stats stats)
    {

        test= stats.test;
        sword = combat.sword;
    }
}

And inside the “Stats” class

    public void SavePlayer()
    {
        SaveSystem.SavePlayer(this);
    }
    public void LoadPlayer()
    {
        PlayerData data = SaveSystem.LoadPlayer();
        test= data.test;
        combat.sword = data.sword;
    }

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)

I made a lot of various test at random but did not find a solution yet :confused:

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.

1 Like

I tried this:

SaveSystem:

using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class SaveSystem
{
    public static void SavePlayer (GameData gamedata)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        string path = Application.persistentDataPath + "/save1.dat";
        FileStream stream = new FileStream(path, FileMode.Create);

        PlayerData data = new PlayerData(gamedata);

        formatter.Serialize(stream, data);
        stream.Close();
    }

    public static PlayerData LoadPlayer()
    {
        string path = Application.persistentDataPath + "/save1.dat";
        if (File.Exists(path))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            FileStream stream = new FileStream(path, FileMode.Open);

            PlayerData data = formatter.Deserialize(stream) as PlayerData;
            stream.Close();

            return data;
        }

        else
        {
            Debug.LogError("Save file not found in " + path);
            return null;
        }
    }
}

PlayerData:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerData
{
    public int level;


    public Stats stats;


 



public PlayerData (GameData gameData)
    {
        level = stats.level;



    }
}

GameData:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameData : MonoBehaviour
{
    public int level;

    public Stats stats;
    public PlayerData playerdata;


    public void SavePlayer()
    {
        SaveSystem.SavePlayer(this);
    }

    public void LoadPlayer()
    {
        PlayerData data = SaveSystem.LoadPlayer();

        level = data.level;


    }
}

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;

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?

2 Likes

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

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:

public GameData(int level, PlayerGameObject player) {
this.level = level;
this.playerdata = new PlayerData(player);
}

PlayerData would have its own constructor that does similar things.

Does this help?

1 Like

Hey :slight_smile:

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:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ISaveAndLoadData
{
    void SaveData();

    void LoadData();
}

The savemanager:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SaveManager : MonoBehaviour
{
    private static SaveManager instance;

    public static SaveManager Instance => instance;

    private List<ISaveAndLoadData> _savers;

    // Create singleton if needed
    private void Awake()
    {
        if(instance != null)
        {
            Destroy(gameObject);
        }
        else
        {
            instance = this;
            _savers = new List<ISaveAndLoadData>();
        }
    }

    // Add a new saving script
    public void AddSaver(ISaveAndLoadData saver)
    {
        if (_savers.Contains(saver))
        {
            return;
        }
        _savers.Add(saver);
    }

    // Saves data on all scripts
    public void TriggerGlobalSaved()
    {
        foreach(ISaveAndLoadData s in _savers)
        {
            s.SaveData();
        }
    }

    // Loads data on all scripts
    public void TriggerGlobalLoad()
    {
        foreach (ISaveAndLoadData s in _savers)
        {
            s.LoadData();
        }
    }
}

An example class:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ExampleSavingClass : MonoBehaviour , ISaveAndLoadData
{
    // Use start to make sure that the save manager has initialized
    private void Start()
    {
        SaveManager.Instance.AddSaver(this);
    }

    public void LoadData()
    {
        // Your implementation for loading data aka. Brackeys approach
        throw new System.NotImplementedException();
    }

    public void SaveData()
    {
        // Your implementation for saving data aka. Brackeys approach
    }
}

Of course you could add different layers of complexity :slight_smile:

1 Like

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”)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class PlayerData
{
    public int level;
    public float currentHealth;
    public float experience;
    public float experienceToNextLevel;
    public float MaximumHealth;
    public int Strength;
    public int tempStrenght;
    public int Agility;
    public int tempAgility;
    public int Charisma;
    public int tempCharisma;
    public int TempStatsPoints;
    public int sword;
    public int rope;
    public int axe;
    public int torch;



 



public PlayerData (Stats stats)
    {
        level = stats.level;
        currentHealth = stats.currentHealth;
        experience = stats.experience;
        experienceToNextLevel = stats.experienceToNextLevel;
        MaximumHealth = stats.MaximumHealth;
        Strength = stats.Strength;
        tempStrenght = stats.tempStrenght;
        Agility = stats.Agility;
        tempAgility = stats.tempAgility;
        Charisma = stats.Charisma;
        tempCharisma = stats.tempCharisma;
        TempStatsPoints = stats.TempStatsPoints;


    }
}

I think this part does 2 and 3

using UnityEngine;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public static class SaveSystem
{
    public static void SavePlayer (Stats stats)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        string path = Application.persistentDataPath + "/save1.dat";
        FileStream stream = new FileStream(path, FileMode.Create);

        PlayerData data = new PlayerData(stats);

        formatter.Serialize(stream, data);
        stream.Close();
    }

    public static PlayerData LoadPlayer()
    {
        string path = Application.persistentDataPath + "/save1.dat";
        if (File.Exists(path))
        {
            BinaryFormatter formatter = new BinaryFormatter();
            FileStream stream = new FileStream(path, FileMode.Open);

            PlayerData data = formatter.Deserialize(stream) as PlayerData;
            stream.Close();

            return data;
        }

        else
        {
            Debug.LogError("Save file not found in " + path);
            return null;
        }
    }
}

And inside another class called Stats, a part of the code is probably doing 4:

    public void SavePlayer()
    {
        SaveSystem.SavePlayer(this);
    }

    public void LoadPlayer()
    {
        PlayerData data = SaveSystem.LoadPlayer();

        level = data.level;
        currentHealth = data.currentHealth;
        experience = data.experience;
        experienceToNextLevel = data.experienceToNextLevel;
        MaximumHealth = data.MaximumHealth;
        Strength = data.Strength;
        tempStrenght = data.tempStrenght;
        Agility = data.Agility;
        tempAgility = data.tempAgility;
        Charisma = data.Charisma;
        tempCharisma = data.tempCharisma;
        TempStatsPoints = data.TempStatsPoints;

     
    }

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”

1 Like

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

1 Like

This is what the Unity experts suggested to me. I did not find it a clean solution either.

1 Like

Wait… are you referring to the lengthy thread we had.

No… that’s not we suggested. lol

Yes. A consequence of your suggested kludge.

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???

:eyes:?

3 Likes

It was indeed what was suggested in the thread I was referencing…

3 Likes