Search code examples
c#oopunity-game-enginebinaryformatter

Unity C# Saved Game via Binary Formatting not retaining Data


Update: This issue may be caused by binaryformatter's issues with editing data in existing fields. A comment that I can no longer find described alternate methods which I am currently implementing. Will update after these methods are attempted if it is a solution to the problem.

I should start by saying I am a student so please go easy on me, I got reported when I first started coming here and hope to keep learning. I an new to unity, my C# is decent, but full of gaps since my schooling was rather terrible. I have watched hundreds of hours of unity tutorials and am studying the new concepts I learn for 4 hours every night after I get out of work, so if you see something just let me know.

This is actually a problem I have had for a while, but thought I fixed months ago. I was attempting to save games for the first time, and read into binary formatting and such to save. I had problems getting it up, but I I managed to get it to save and pull properly from a file. I verify that the data going into the file is correct, and the data coming out is correct, and even made the data private with a control function so nothing will access and change it without jumping through my debug. And yet after I leave the scope where I define the data it changes, without anything accessing my update function.

To break it down I have a class called PlayerType that stores all player information including my scenemanager, and it serializes this and saves to a file as a list. I create a for loop using the current length of the loaded list using an instance of the saveload class (this is what holds the list of save games and the access to the file) and it loops through instantiating my buttons in the order. Slot 1 will click to save game 1 and so to speak. The issue I am having is clicking slot 1 clicks slot 16, so does slot 2. In face, it seems practically random which buttons go to which slot. I should say here I am not sure whether it is actually going to the wrong slot, or simply renaming the player names wrong, but either way it does not appear this should be the case.

Here is my load function

public void Load() //Loads file from .gd file after checking if exists, then deserializes it back into a list
{
    accessDataPath(false);

    Debug.Log("Size of Save File" + listSize);

    for (int i = 0; i < listSize; i++)
    {
        Debug.Log("First Loop " + SavedGames[i].returnName());
    }

    foreach (PlayerType player in GameManager.instance.saveStorage.returnList())
    {

        Debug.Log("Second Loop" + player.returnName());
    }
}

and here is my accessDataPath function

public void accessDataPath(bool isSave)
{
    //This stucture checks if file exists or not, then checks if saving or loading for 4 outcomes


    if (isSave && SavedGames == null)//if saving and save file to update has nothing in it
    {
        Debug.Log("Error attempted to save null list in SaveLoad.accessDataPath!");
    }


    if (File.Exists(Application.persistentDataPath + "/SavedGames.gd"))//if the file exists
    {
        if (isSave)//if you are saving
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open(Application.persistentDataPath + "/SavedGames.gd", FileMode.Open);
            bf.Serialize(file, SavedGames);
            file.Close();
        }

        else //if you are not saving
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open(Application.persistentDataPath + "/SavedGames.gd", FileMode.Open);
            SavedGames = (List<PlayerType>)bf.Deserialize(file);
            file.Close();
            listSize = SavedGames.Count;
        }
    }
    else//if the file does not exist
    {
        if (isSave)//if you are saving
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Create(Application.persistentDataPath + "/SavedGames.gd");
            bf.Serialize(file, SavedGames);
            file.Close();
        }
        else//if you are not saving
            Debug.Log("Error Loading File. Does not Exist!");//Display Later that file does not exist to user
    }
    updateNames();
}//End accessDataPath

here is the function that updates the names

public void updateNames()
{
    for (int i = 0; i < listSize; i++)
    {
        SavedGames[i].updateName("Player " + i);
        Debug.Log("Bump " + i);
    }
}

As you can see I verify if it exists, then check if I am saving or loading. The save function calls with a true and SavedGames is the list that is pulled from the file. Now After I run the name change I can check to see if it worked, and it does. Running the check here they all come back as the proper name, however by the time it gets back to my load function and runs my first loop they are wrong, it never leaves this section of code but as soon as it exits the scope they change to be almost random. Now I had this problem for a while, but thought it had to do with my file maybe having corrupt data, so I deleted the save game and created new ones to test. The numbers changed, but were still not right. This tells me that somehow the file is effecting the names even though I rename them almost immediately after they come out. I am not used to loading or saving to files so I am not sure where to start here.

I will post more of my code below in case some of it stands out as being blatantly wrong, I also appreciate advice on structure as I took college classes on game programming but they were terrible and filled with gaps even if you don't spot the answer to my problem.

First I have my menu system setup to be toggled on or off, called by onclick events.

public class SaveMenu : MonoBehaviour {
public GameObject menu;
bool isActive;
bool isSave;
PopulateSave playerSaves;


public static SaveMenu instance;


void Awake()
{
    isSave = false;
    instance = this;
    menu = GameObject.Find ("SaveList");
    menu.SetActive (false);
    isActive = false;
    playerSaves = menu.GetComponentInChildren<PopulateSave> ();
}

public bool getActive()
{
    return isActive;
}
public void toggleMenu(bool saveLoad)
{
    if (isActive) 
    {
        menu.SetActive (false);
        isActive = false;
    } else 
    {
        menu.SetActive (true);
        isActive = true;
    }
    bool isSave = saveLoad;
    menu.transform.Find("Scroll View").transform.Find("Viewport").transform.Find("Content").transform.Find("NewSave").gameObject.SetActive(isSave);
    Debug.Log(isSave);


}
public void updateSave()
{
    playerSaves.startList();//Seems redundant but is used to make this access public with limited use
}
public bool getSave()
{
    return isSave;
}
}

I call populatesaves as a child after everything is created because I had issues with it not existing when I tried to bring it in via the inspector. PopulateSaves is where most of my functional code is.

public class PopulateSave : MonoBehaviour{
public Button NewSave;
public Button OldSaves; // This is our prefab object that will be exposed in the inspector



void Awake()
{
    GameManager.instance.saveStorage.Load();

    Populate();
}

void Update()
{

}

void Populate()
{
    Button newObj = Instantiate(NewSave, transform); // Create GameObject instance

    newObj.name = "NewSave";

    startList();

}
public void startList()
{
    clearList();

    for (int i = 1; i <  GameManager.instance.saveStorage.returnCount() + 1; i++)
    {
        createButton(i);
    }
    Debug.Log("Done creating");
}
public void updateList(PlayerType newSave)
{
    Button newObj;
    newObj = (Button)Instantiate(OldSaves, transform);
    //GameManager.instance.saveStorage.Save(i);
}
public void clearList()
{
    GameObject[] gameObjects;
    gameObjects = GameObject.FindGameObjectsWithTag("SaveSlot");
    for (var i = 0; i < gameObjects.Length; i++)
        Destroy(gameObjects[i]);
    Debug.Log("Done Destroying");
}
public void ButtonClicked(int slot)
{

    if (SaveMenu.instance.getSave())
    {
        Debug.Log("Save");
        GameManager.instance.saveStorage.Save(slot);
    }
    else
    {
        Debug.Log("load");
        GameManager.instance.currentPlayer.newPlayer(slot);
    }

}
public void createButton(int i)
{

    // Create new instances of our prefab until we've created as many as is in list
    Button newObj = (Button)Instantiate(OldSaves, transform);

    //increment slot names
    newObj.name = "Slot " + i;
    newObj.GetComponentInChildren<Text>().text = "Slot " + i;
    newObj.onClick.AddListener(() => ButtonClicked(i));
}
}

Now I also have never written a listener with a script like I have here, could that section be assigning the wrong number to them? I checked to make sure all my indexes had the right numbers, if not being 1 off. One of my loops might use a setup that is 1 off but I am more worried about getting this off the ground and fixing the specifics at this point.

Here is my player storage

[System.Serializable]
public class PlayerType {
string playerName;
SceneManagement currentScene;

public PlayerType()
    {
    playerName = "Starting Name";
    }
public void updateName(string name)
{
    //Debug.Log("New Name: " + name);
    playerName = name;
}
public void updateScene(SceneManagement newScene)
{
    currentScene = newScene;
}
public string returnName()
{
    return playerName;
}
public SceneManagement returnScene()
{
    return currentScene;
}

public void newPlayer(int slotSave)
{

    //Tmp player has wrong name from this point, initiated wrong?

    //Debug.Log("New Name to update" + tmpPlayer.returnName());
    this.updateName(GameManager.instance.saveStorage.returnSaves(slotSave).returnName());
    this.updateScene(GameManager.instance.saveStorage.returnSaves(slotSave).returnScene());
}
}

Update: Bump correctly goes through displaying 0-16 then Size of Save File displays 17 total saves First loop then starts by outputting 'First Loop Player 15' a total of 16 times Then it displays 16 so the last one is correct, though one off I guess. Second loop does the same as first, unsurprisingly.

I left the call to updateNames in but commented out the lines and ran it including taking out bump. It starts with 17 saves again and the 16 time iteration of player 15, however this time around the last one displays 'Temp Name' which I only define once at the beginning of my sceneManagement script for the current player, it should have never been saved, and even if it had been should have been overwritten by my name loop, at least that was my intent.

SceneManagement

[System.Serializable] 
public class SceneManagement : MonoBehaviour {

DialogueManager dialogueManager;
PlayerType currentPlayer;

bool isSave;
bool isActive;
string sceneName;
int lineCount;


void Start()
{
    isSave = false;
    //loads player object for the current save
    currentPlayer = new PlayerType();
    currentPlayer.updateName ("Temp Name");



    //This loads the prologue from the DB and sets the dialoguemanager up, defaults to prologue for now but can be updated to another scene later
    isActive = true;
    sceneName = "Prologue";
    string conn = "URI=file:" + Application.dataPath + "/Text/Game.sqlite3";

    List<string> sceneLines = new List<string>();
    List<string> sceneCharacters = new List<string>();
    int tmpInt;
    string tmpString = "NOTHING";
    int count = 0;
    lineCount = 1;

    IDbConnection dbConn;
    dbConn = (IDbConnection)new SqliteConnection (conn);
    dbConn.Open (); //Open database connection
    IDbCommand dbCmd = dbConn.CreateCommand();
    string sqlQuery = "SELECT Line, Flags, Character, Image, Text, Color FROM Prologue";
    dbCmd.CommandText = sqlQuery;
    IDataReader reader = dbCmd.ExecuteReader ();


    while (reader.Read ()) {
        tmpInt = reader.GetInt32 (0);
        tmpString = reader.GetString (1);
        sceneCharacters.Add(reader.GetString (2));
        tmpInt = reader.GetInt32 (3);
        sceneLines.Add(reader.GetString (4));
        tmpString = reader.GetString (5);

        //Debug.Log (tmpString);
        //Debug.Log (count);
        count++;
    }


    reader.Close ();
    reader = null;
    dbCmd.Dispose ();
    dbCmd = null;
    dbConn.Close ();
    dbConn = null;



    dialogueManager = new DialogueManager(sceneCharacters, sceneLines, lineCount);
}
//These are the returnvalues that might be used, may or may not be kept depending on future use.
public string getSceneName()
{
    return sceneName;
}
public int getLineCount()
{
    return lineCount;
}
public bool getSave()
{
    return isSave;
}
//These return the lines for displaymanager, preventing it from directly interacting with dialoguemanager
public string getNextLine()
{
    return dialogueManager.getNextLine();
}
   public string getNextCharacter()
{
    return dialogueManager.getNextCharacter();
}

//This function sets up visibility and is only accessed to properly display the screen after the scene has been started.
public void startScene ()
{
    GameManager.instance.screenDisplay.changeVisible(true);
}
void Update()
{
    if (Input.GetKeyDown ("space") & isActive) {
        dialogueManager.incrementCurrentLine ();
        GameManager.instance.screenDisplay.changeText(dialogueManager.getNextLine ());
        GameManager.instance.screenDisplay.changeCharacter(dialogueManager.getNextCharacter ());
    }




    if (Input.GetKeyDown ("escape")) {
        if (!PauseMenu.instance.getActive () && !SaveMenu.instance.getActive ()) 
        {
            PauseMenu.instance.toggleMenu ();
        } 
        else if (SaveMenu.instance.getActive()) 
        {
            SaveMenu.instance.toggleMenu (true);
            PauseMenu.instance.toggleMenu ();
        }
        else if (PauseMenu.instance.getActive())
        {
            PauseMenu.instance.toggleMenu ();
        }
    }

}
public void initialScreen()
{
    SceneManager.sceneLoaded += OnLevelFinishedLoading;
}
void OnDisable()
{
    SceneManager.sceneLoaded -= OnLevelFinishedLoading;

}
void OnLevelFinishedLoading (Scene scene, LoadSceneMode mode)
{
    GameManager.instance.screenDisplay.initialScreen(dialogueManager.getNextLine(), dialogueManager.getNextCharacter(), null, null,
        null, null, null, null);
}
public PlayerType returnPlayer()
{
    return currentPlayer;
}
}

while im at it here is my GameManager too, though I havent messed with it a ton. Mostly using it as a DontDestroyOnLoad thing to hold everything else at this point.

public class GameManager : MonoBehaviour{

public static GameManager instance;
public DisplayScreen screenDisplay;
public SceneManagement managerScene;
public SaveLoad saveStorage;
public PlayerType currentPlayer;
//leave out until load data is setup
//PlayerType currentPlayer; 



void Awake()
{
    instance = this;
    DontDestroyOnLoad (transform.gameObject);
    managerScene = gameObject.AddComponent(typeof(SceneManagement)) as SceneManagement;
    screenDisplay = gameObject.AddComponent (typeof(DisplayScreen)) as DisplayScreen;
    saveStorage = gameObject.AddComponent (typeof(SaveLoad)) as SaveLoad;

    //initialize player and sceneManager here, but only load through main menu options
    //of new game or load game
}

void update()
{

}
}

I have continued troubleshooting and brought it down to a particularly confusing part for me. I took out all debugging logs and added 3 loops into my load function, they are these.

Debug.Log("LOAD CALLED");

    accessDataPath(false);

    for (int i = 0; i < listSize; i++)
    {
        Debug.Log("First Loop " + SavedGames[i].returnName());
        SavedGames[i].updateName("Updated Player " + (i + 1));

    }

    for (int i = 0; i < listSize; i++)
    {
        Debug.Log("Second Loop " + SavedGames[i].returnName());

    }

    foreach (PlayerType player in GameManager.instance.saveStorage.returnList())
    {

        Debug.Log("Third Loop " + player.returnName());
    }

So the first one displays player 15, then correctly displays the first loops 1-15 then temp name again, still havent 'figured out why that is popping up but I think it is related. Then the second loop iterates all wrong. Literally changed and back to back the loops are wrong, the only difference being it left the scope of the for loop. I ran a third loop using foreach to see if the type of call made a difference and it does not.

Even changing the name, and then immediately calling a loop to check shows that the values are changing. I think it might have something to do with how I am storing the objects, but I am not sure how the problem could be arising. Its not going null, and the names are changing to be the same every time so it isn't completely random. I am posting this here just after I found this hoping that soon I will solve it, but am also hoping if I do not someone else might spot something. I have the next 3 hours to work on it so I will be trying this entire time checking back every now and then. Thanks in advance for anyone that might glance at it for me.


Solution

  • Ok so I finally got the implementation done. I had to swap around a ton of my code, and ended up doing a wrapper object/list combination using JSON. Essentially the problem is that binary formatter messes up objects after you de-serialize them. Every time I updated my objects values they would randomly change on me for no reason without being accessed.

    This was surprising as about half the posts I read on saving say its good, others like This one say that it is bad practice. I had done some research initially but had not come across the negative aspects of using it. I am still having problems, but Json is successfully retaining data and my objects are not messing up. I am pretty sure this was the problem as the only sections that I changed were my objects value structure to public for the serializing, and implemented the json structure into my SaveLoad script. This video was very helpful for the overall structure and getting started, and This thread helped me with troubleshooting when I ran into several problems.

    I should also note that one thing I did not catch for a while. While Json can load lists, the initial object to be loaded must not be a list. I was attempting to save a list of my PlayerType directly into a folder, which it will not do. I ended up creating a quick object that contained my list and then saving the object. Since everywhere I read said that lists were fine it took a while to discover that this was causing part of my problem. It was not giving me any errors, just returning a blank string which most threads said was because it was not public or serializable.

    Anyway here is to hoping my struggles and searches for answers might help as the things I found were quite scattered and hard to come across.