0

I am using a binary formatter in my Unity game to store the progress of the player to keep track which level(s) he unlocked. It all works fine with this code:

public static void Save()
{
    LevelManager levelManager = GameObject.Find ("GameManager").GetComponent<LevelManager> ();
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = File.Create (Application.persistentDataPath + "/savedProgress.1912");
    bf.Serialize(file, levelManager.levelReached);
    file.Close();
}

public static void Load() 
{
    LevelManager levelManager = GameObject.Find ("GameManager").GetComponent<LevelManager> ();
    if(File.Exists(Application.persistentDataPath + "/savedProgress.1912")) 
    {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(Application.persistentDataPath + "/savedProgress.1912", FileMode.Open);
        levelManager.levelReached = (int)bf.Deserialize(file);
        file.Close();
    }
}

It just saves and int called levelReached. After the progress is saved, there will be added a new file onto your computer, in this case "savedProgress.1912".

If I edit the file as text, it looks like this:

https://i.gyazo.com/e604f83666f14424b6d45d15b16afacc.png

Whenever I replace or delete something in this file, it will be corrupted and give me the following error in Unity:

System.Runtime.Remoting.Messaging.Header[]& headers) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/ObjectReader.cs:104)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.NoCheckDeserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/BinaryFormatter.cs:179)
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream) (at /Users/builduser/buildslave/mono/build/mcs/class/corlib/System.Runtime.Serialization.Formatters.Binary/BinaryFormatter.cs:136)
SaveLoadProgress.Load () (at Assets/Scripts/SaveLoadProgress.cs:25)
LevelManager.Start () (at Assets/Scripts/LevelManager.cs:16)

The problem with this is, is that when I do this, I unlock all levels. They will go back to the state in which they are in the editor, before playing it. I know that I can make them locked as standard state, but is there maybe a way to not let this happen in another way?

Edit: When I wrote this I realised what I just said, just make all the level locked by standard and the problem is fixed. Though it would still be nice to know.


EDIT: I am know using the code from this thread.

This is what I'm currently using:

DataSaver.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Text;
using System;


public class DataSaver
{
    //Save Data
    public static void saveData<T>(T dataToSave, string dataFileName)
    {
        string tempPath = Path.Combine(Application.persistentDataPath, "data");
        tempPath = Path.Combine(tempPath, dataFileName + ".txt");

        //Convert To Json then to bytes
        string jsonData = JsonUtility.ToJson(dataToSave, true);
        byte[] jsonByte = Encoding.ASCII.GetBytes(jsonData);

        //Create Directory if it does not exist
        if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(tempPath));
        }
        //Debug.Log(path);

        try
        {
            File.WriteAllBytes(tempPath, jsonByte);
            Debug.Log("Saved Data to: " + tempPath.Replace("/", "\\"));
        }
        catch (Exception e)
        {
            Debug.LogWarning("Failed To PlayerInfo Data to: " + tempPath.Replace("/", "\\"));
            Debug.LogWarning("Error: " + e.Message);
        }
    }

    //Load Data
    public static T loadData<T>(string dataFileName)
    {
        string tempPath = Path.Combine(Application.persistentDataPath, "data");
        tempPath = Path.Combine(tempPath, dataFileName + ".txt");

        //Exit if Directory or File does not exist
        if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
        {
            Debug.LogWarning("Directory does not exist");
            return default(T);
        }

        if (!File.Exists(tempPath))
        {
            Debug.Log("File does not exist");
            return default(T);
        }

        //Load saved Json
        byte[] jsonByte = null;
        try
        {
            jsonByte = File.ReadAllBytes(tempPath);
            Debug.Log("Loaded Data from: " + tempPath.Replace("/", "\\"));
        }
        catch (Exception e)
        {
            Debug.LogWarning("Failed To Load Data from: " + tempPath.Replace("/", "\\"));
            Debug.LogWarning("Error: " + e.Message);
        }

        //Convert to json string
        string jsonData = Encoding.ASCII.GetString(jsonByte);

        //Convert to Object
        object resultValue = JsonUtility.FromJson<T>(jsonData);
        return (T)Convert.ChangeType(resultValue, typeof(T));
    }

    public static bool deleteData(string dataFileName)
    {
        bool success = false;

        //Load Data
        string tempPath = Path.Combine(Application.persistentDataPath, "data");
        tempPath = Path.Combine(tempPath, dataFileName + ".txt");

        //Exit if Directory or File does not exist
        if (!Directory.Exists(Path.GetDirectoryName(tempPath)))
        {
            Debug.LogWarning("Directory does not exist");
            return false;
        }

        if (!File.Exists(tempPath))
        {
            Debug.Log("File does not exist");
            return false;
        }

        try
        {
            File.Delete(tempPath);
            Debug.Log("Data deleted from: " + tempPath.Replace("/", "\\"));
            success = true;
        }
        catch (Exception e)
        {
            Debug.LogWarning("Failed To Delete Data: " + e.Message);
        }

        return success;
    }
}

LevelManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.Linq;
using UnityEngine.UI;

[System.Serializable]
public class LevelManager : MonoBehaviour {

    public GameObject[] levelButtons; 
    public int levelReached = 1;

    void Start()
    {
        //SaveLoadProgress.Load ();
        LevelManager loadedData = DataSaver.loadData<LevelManager>("Progress");
        if (loadedData == null)
        {
            return;
        }

        levelButtons = GameObject.FindGameObjectsWithTag ("Level").OrderBy (go => go.name).ToArray ();
        for (int i = 0; i < levelButtons.Length; i++) 
        {
            if (i + 1 > loadedData.levelReached) 
            {
                Color greyed = levelButtons [i].GetComponent<Image> ().color;
                greyed.a = 0.5f;
                levelButtons [i].GetComponent<Image> ().color = greyed;
            }
            if (i < loadedData.levelReached) 
            {
                Color normal = levelButtons [i].GetComponent<Image> ().color;
                normal.a = 1f;
                levelButtons [i].GetComponent<Image> ().color = normal;
            }
        }
    }

}

GameController.cs

if(--LEVELCOMPLETED--)
{
    LevelManager saveData = new LevelManager();
    saveData.levelReached = SceneManager.GetActiveScene ().buildIndex + 1;
    DataSaver.saveData(saveData, "Progress");
}

When I use these codes, the debug log shows: Saved data to... and loaded data from.. but after the loaded data from.., it gives me this error:

ArgumentException: Cannot deserialize JSON to new instances of type 'LevelManager.'
UnityEngine.JsonUtility.FromJson[LevelManager] (System.String json) (at C:/buildslave/unity/build/artifacts/generated/common/modules/JSONSerialize/JsonUtilityBindings.gen.cs:25)
DataSaver.loadData[LevelManager] (System.String dataFileName) (at Assets/Scripts/DataSaver.cs:77)
LevelManager.Start () (at Assets/Scripts/LevelManager.cs:17)

The file "Progress.txt" does excist. It looks like this:

{
    "levelButtons": [],
    "levelReached": 2
}

Are modders able to see this file when I publish it for android or apple? Do I need to encrypt this if I don't want people to be able to cheat? How do I do this?

J.K. Harthoorn
  • 218
  • 1
  • 3
  • 19
  • 1
    I would be very careful about storing a save as a binary file. Many games have done this in the past and when those games were updated/patched, the old saves were not compatible afterwards. All it takes is inserting 1 byte anywhere other than the end to throw it off. I would try to save it in some sort of structure (XML, JSON, etc.) and use encryption if you didn't want people messing with the file. – TyCobb Oct 30 '17 at 22:16
  • Why are they not compatible after patching? I searched it up and almost everyone that uses unity suggest this method. – J.K. Harthoorn Oct 30 '17 at 22:18
  • It depends on the game and what you are saving, but imagine saves that are megabytes in size. You add any new data before the end and that save is broken. It's also way easier to manage a structure than bytes. – TyCobb Oct 30 '17 at 22:20
  • Ok I understand. So what you are saying is that the files are very fragile. At the moment I only store one single interger which updates after completing a level. Do you think it could go wrong? – J.K. Harthoorn Oct 30 '17 at 22:21
  • 2
    @TyCobb is right about `BinaryFormatter` Read [this](https://stackoverflow.com/questions/40965645/what-is-the-best-way-to-save-game-state/40966346#40966346) answer from here. I explained that too and showed how json is used to do this instead. A simple modification of the class/struct you serializing can break your old saved data which means you can't load it. You will have to find a way to handle that if you decide to use `BinaryFormatter` – Programmer Oct 30 '17 at 22:21
  • That could very well be fine. I would just watch out for when you start doing stuff like storing Player Name, level completed, time completed, etc. It starts to get hard to manage and you have to be very cautious as size and order matters. A single integer? There's nothing to worry about with that. – TyCobb Oct 30 '17 at 22:24
  • Thanks for the information. I think that I will just do the safe way and take a look at your answer on the other thread ;) – J.K. Harthoorn Oct 30 '17 at 22:31
  • This isn't good enough for an answer, but your actual issue is that you probably want to use the `BinaryWriter` and the `BinaryReader` class if it is available. `BinaryFormatter` wants to serialize/deserialize actual objects, not data. Hence the reason you fed it an integer value and you see `System.Int32` in your file ;) – TyCobb Oct 30 '17 at 22:32
  • `BinaryWriter` and `BinaryReader` are available in Unity but I haven't used them and can't say if they don't have the-same problem. – Programmer Oct 30 '17 at 22:42
  • @Programmer Oh they do. But it's what allows you literally write just the binary data you were wanting i.e. just saving an int would yield only a 4 byte file. – TyCobb Oct 30 '17 at 22:44
  • Ok. If you modify the class/structure, can you still load the old data with these? When I say modify, I mean add/remove fields to the structure/class... – Programmer Oct 30 '17 at 22:46
  • 1
    @Programmer Having been messing around with the BinaryFormatter, I can say that yes, I can handle old save files when there are new fields. I can handle this by starting the save file with a version number. Then when loading I compare that number with the value that number was when new data got added to the file. If the version number is high enough, read, else skip. I would still avoid editing the file manually and I'd still recommend using JSON, but this format *is* flexible if you know what you're doing. – Draco18s no longer trusts SE Oct 31 '17 at 23:24
  • @Draco18s Thanks for the update. I've ran into so many issues with `BinaryFormatter` like not being able to load old data when the structure/class is modified. I also found many people having the-same issue. Maybe something has changed but it's good to know many people got it working. – Programmer Nov 01 '17 at 01:03
  • Notably, I'm explicitly implementing the `ISerializable` interface (`GetObjectData(SerializationInfo info, StreamingContext context)` and `public Whatever(SerializationInfo info, StreamingContext context)`) in order to do this. – Draco18s no longer trusts SE Nov 01 '17 at 01:27
  • I used the technique from [here](https://stackoverflow.com/questions/40965645/what-is-the-best-way-to-save-game-state/40966346#40966346) but I am getting this error `ArgumentException: Cannot deserialize JSON to new instances of type 'LevelManager.' UnityEngine.JsonUtility.FromJson[LevelManager] (System.String json) (at C:/buildslave/unity/build/artifacts/generated/common/modules/JSONSerialize/JsonUtilityBindings.gen.cs:25) DataSaver.loadData[LevelManager] (System.String dataFileName) (at Assets/Scripts/DataSaver.cs:77) LevelManager.Start () (at Assets/Scripts/LevelManager.cs:17)` – J.K. Harthoorn Nov 01 '17 at 22:05
  • Any idea? @Programmer – J.K. Harthoorn Nov 03 '17 at 16:59
  • @J.K.Harthoorn You have to add "EDIT" in your question then add the class you are serializing and how you are serializing/de-serializing it with my `DataSaver` script otherwise it would be hard to help you without those. – Programmer Nov 03 '17 at 17:14
  • @Programmer I edited it :). – J.K. Harthoorn Nov 05 '17 at 10:54
  • Any idea? @Programmer – J.K. Harthoorn Nov 08 '17 at 09:51
  • See the example I linked. You must have a dedicated class that you use to store game value. This class must **not** inherit from `MonoBehaviour`. It should not inherit from anything. Again, see the [example](https://stackoverflow.com/questions/40965645/what-is-the-best-way-to-save-game-state/40966346#40966346) of a class I serialized and de-serialized. Move your `public GameObject[] levelButtons;` and `public int levelReached = 1;` into that new class that doesn't inherit from `MonoBehaviour`. In fact, inheriting from `MonoBehaviour` may be the reason you have issues with `BinaryFormatter `. – Programmer Nov 08 '17 at 10:04
  • Thanks, that did the trick! Are modders able to see this file when I publish it for android or apple? Do I need to encrypt this if I don't want people to be able to cheat? How do I do this? Atm, I can change the text file which changes the int. – J.K. Harthoorn Nov 08 '17 at 13:15
  • Any game can be hacked. Any saved data can be hacked. You can [encrypt](https://stackoverflow.com/questions/202011/encrypt-and-decrypt-a-string) it and save it to a server but it can still be hacked if the person decompile the app. – Programmer Nov 08 '17 at 20:05
  • @Programmer Ive taken a look at the thread multiple times but I just don't know which one to take and also it looks like all of them are made for storing strings, but I need intergers. – J.K. Harthoorn Nov 10 '17 at 08:33

0 Answers0