Unity C# 通过二进制格式保存的游戏不保留数据

Unity C# Saved Game via Binary Formatting not retaining Data

更新:这个问题可能是由于 binaryformatter 在编辑现有字段中的数据时出现的问题。我再也找不到描述我目前正在实施的替代方法的评论。如果可以解决问题,将在尝试这些方法后更新。

首先我应该说我是一名学生所以请宽容我,我刚开始来这里就被举报了,希望继续学习。我是 Unity 的新手,我的 C# 很不错,但由于我的学校教育相当糟糕,所以充满了差距。我已经看了数百小时的 unity 教程,每天晚上下班后都会花 4 个小时研究我学到的新概念,所以如果你看到了什么,请告诉我。

这实际上是我遇到了一段时间的问题,但我想我几个月前就解决了。我是第一次尝试保存游戏,然后读入二进制格式等进行保存。我在启动它时遇到了问题,但我设法让它保存并从文件中正确提取。我验证进入文件的数据是正确的,并且输出的数据是正确的,甚至使用控制功能将数据设为私有,因此如果不跳过我的调试,任何人都无法访问和更改它。然而,在我离开我定义数据的范围后,它发生了变化,没有任何东西访问我的更新功能。

为了分解它,我有一个名为 PlayerType 的 class,它存储所有播放器信息,包括我的场景管理器,并将其序列化并作为列表保存到文件中。我使用 saveload class 的实例使用加载列表的当前长度创建了一个 for 循环(这是保存游戏列表和访问文件的内容),它循环实例化我的按钮命令。插槽 1 将单击以保存游戏 1,可以这么说。我遇到的问题是单击插槽 1 会单击插槽 16,插槽 2 也是如此。从表面上看,实际上 运行dom 哪个按钮转到哪个插槽。我应该在这里说我不确定它是否真的进入了错误的位置,或者只是将玩家名称重命名错误,但无论哪种方式都不会出现这种情况。

这是我的加载函数

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());
    }
}

这是我的 accessDataPath 函数

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

这是更新名称的函数

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

如您所见,我验证它是否存在,然后检查我是在保存还是加载。使用 true 调用保存函数,SavedGames 是从文件中提取的列表。现在,在我 运行 名称更改后,我可以检查它是否有效,它确实有效。 运行 在这里检查它们都作为正确的名称返回,但是当它返回到我的加载函数和 运行 我的第一个循环时它们是错误的,它永远不会离开这部分代码但是一旦它退出范围,它们就会变成几乎 运行dom。 现在我有一段时间遇到这个问题,但认为它与我的文件有关,可能有损坏的数据,所以我删除了保存游戏并创建了新的游戏来测试。数字变了,但仍然不对。这告诉我文件以某种方式影响了名称,即使我几乎在它们出来后立即重命名它们。我不习惯加载或保存到文件,所以我不知道从哪里开始。

我会在下面 post 更多我的代码,以防其中一些明显错误,我也很感激我上大学 class 时关于结构的建议,但他们即使您没有找到我的问题的答案,也很糟糕并且充满了空白。

首先,我将菜单系统设置为打开或关闭,由 onclick 事件调用。

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;
}
}

我在创建所有内容后将 populatesaves 称为 child,因为当我尝试通过检查器将其引入时,我遇到了不存在的问题。 PopulateSaves 是我大部分功能代码所在的地方。

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));
}
}

现在我也从来没有用我在这里的脚本编写过监听器,那部分会不会给他们分配了错误的号码?我检查以确保我所有的索引都有正确的数字,如果不是 1。我的一个循环可能使用了 1 off 的设置,但我更担心的是在这一点上实现它并修复细节。

这是我的播放器存储空间

[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());
}
}

更新: 凹凸正确显示 0-16 然后 Size of Save File 显示 17 total saves 第一个循环然后开始输出 'First Loop Player 15' 总共 16 次 然后它显示 16 所以最后一个是正确的,虽然我猜是一个错误。 毫不奇怪,第二个循环与第一个循环相同。

我留下了对 updateNames 的调用,但注释掉了这些行,运行 它包括去掉了碰撞。 它再次以 17 次保存和玩家 15 的 16 次迭代开始,但是这次在最后一次显示 'Temp Name' 时,我只在当前玩家的 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;
}
}

虽然我在这里也是我的 GameManager,但我并没有把它弄得一团糟。目前主要将其用作 DontDestroyOnLoad 来保存其他所有内容。

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()
{

}
}

我继续进行故障排除,并将其归结为一个让我特别困惑的部分。我取出所有调试日志并在我的加载函数中添加了 3 个循环,它们是这些。

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());
    }

所以第一个显示播放器 15,然后正确显示第一个循环 1-15,然后再次显示临时名称,仍然没有弄清楚为什么会弹出,但我认为它是相关的。然后第二个循环迭代都错了。从字面上改变和背靠背循环是错误的,唯一的区别是它离开了 for 循环的范围。我 运行 第三个循环使用 foreach 来查看调用类型是否有所不同,但事实并非如此。

即使更改名称,然后立即调用循环来检查是否显示值正在更改。我认为这可能与我存储 objects 的方式有关,但我不确定问题是如何产生的。它不会为空,而且名称每次都会更改为相同,因此它并不完全 运行dom。我 post 在我发现这个问题后就在这里,希望我能尽快解决它,但我也希望如果我不这样做,其他人可能会发现一些东西。我有接下来的 3 个小时来处理它,所以我会尝试在这整个时间里时不时地检查一下。提前感谢任何可能帮我看一眼的人。

好的,我终于完成了实施。我不得不交换大量代码,最后使用 JSON 进行包装器 object/list 组合。本质上,问题是二进制格式化程序在反序列化对象后弄乱了对象。每次我更新我的对象值时,它们都会 运行 在我没有被访问的情况下无缘无故地改变我。

这令人惊讶,因为我阅读的关于保存的帖子中大约有一半说它很好,其他人如 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 线程在我 运行 遇到几个问题时帮助我解决了问题。

我还应该注意到一件事我有一段时间没有抓住。虽然 Json 可以加载列表,但要加载的初始对象不能是列表。我试图将我的 PlayerType 列表直接保存到一个文件夹中,但它不会这样做。我最终创建了一个包含我的列表的快速对象,然后保存了该对象。因为我读到的所有地方都说列表很好,所以花了一段时间才发现这是我问题的一部分。它没有给我任何错误,只是返回一个空白字符串,大多数线程说这是因为它不是 public 或可序列化。

无论如何,希望我的努力和寻找答案的努力能有所帮助,因为我发现的东西非常分散,很难找到。