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

时间:2018-10-17 21:38:38

标签: c# oop unity3d binaryformatter

更新:此问题可能是由于在现有字段中编辑数据的二进制格式程序问题。我找不到的评论描述了我当前正在实现的替代方法。如果可以解决问题,将在尝试这些方法后更新。

我首先要说我是一名学生,所以请放心,我第一次来这里时就收到了报道,希望继续学习。我是一个刚接触团结的人,我的C#不错,但是由于我的学校很糟糕,所以充满了空白。我已经看了数百小时的统一教程,并且正在每天下班后每晚4个小时学习新概念,所以,如果您看到什么,请告诉我。

这实际上是我一段时间以来遇到的问题,但以为我几个月前已解决。我是第一次尝试保存游戏,然后读入二进制格式并进行保存。我在安装它时遇到了问题,但是我设法将其保存并从文件中正确提取。我验证输入到文件中的数据是正确的,并且输出的数据是正确的,并且甚至通过控制功能将数据设为私有,因此在不跳过调试的情况下,任何人都无法访问和更改它。但是,在离开范围定义数据的范围之后,无需访问更新功能。

要对其进行分解,我有一个名为PlayerType的类,该类存储包括我的场景管理器在内的所有播放器信息,并将其序列化并将其保存为列表文件。我使用saveload类的实例(使用保存游戏列表和文件访问权限)使用加载列表的当前长度创建一个for循环,并按顺序实例化我的按钮。插槽1将单击以保存游戏1,可以这么说。我遇到的问题是单击插槽1会单击插槽16,插槽2也会单击。表面上,实际上哪个按钮转到哪个插槽实际上是随机的。我应该在这里说我不确定是将其插入错误的插槽,还是只是将播放器名称重命名为错误的名称,但是无论哪种方式都不会出现这种情况。

这是我的加载功能

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是从文件中拉出的列表。现在,在运行名称更改后,我可以检查它是否有效,并且可以。在这里运行检查,它们全都以正确的名称返回,但是当它回到我的加载函数并运行我的第一个循环时,它们是错误的,它永远不会离开这部分代码,但是一旦退出范围,它们就会改变几乎是随机的。 现在我有一段时间这个问题了,但是我认为这与我的文件可能包含损坏的数据有关,因此我删除了保存游戏并创建了新游戏进行测试。数字有所变化,但仍然不正确。这告诉我,即使我在名称出来后几乎立即将其重命名,文件也会以某种方式影响名称。我不习惯加载或保存到文件,所以我不确定从哪里开始。

我将在下面发布更多代码,以防某些代码明显错误,我也很欣赏结构方面的建议,因为我上了大学的游戏编程课程,但即使您不这样做,它们也很糟糕并且充满空白找不到我问题的答案。

首先,我要通过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称为孩子,因为当我尝试通过检查器将其放入时,我不存在它的问题。我的大多数功能代码都在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,但是我更担心此设置并确定具体细节。

这是我的播放器存储空间

[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 然后“保存文件的大小”显示总共17次保存 然后,首先循环输出“ First Loop Player 15”,总共进行16次 然后它显示16,所以最后一个是正确的,尽管我猜是一个。 毫不奇怪,第二个循环的作用与第一个相同。

我保留了对updateNames的调用,但是注释了所有行并运行了它,包括删除了凹凸。 它再次以17次保存开始,并且播放器15进行了16次迭代,但是这一次在最后一个播放器周围显示了“临时名称”,我仅在我的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进行了第三个循环,以查看调用的类型是否有所不同,是否没有变化。

即使更改了名称,然后立即调用循环以检查显示值正在更改。我认为这可能与我存储对象的方式有关,但是我不确定问题可能如何产生。它不会为空,并且名称每次都更改为相同,因此不是完全随机的。我在找到这个希望后不久便将其发布在这里,希望很快我能解决它,但也希望如果没有其他人可以发现一些东西。接下来的3个小时,我将继续工作,因此,我将不时地尝试整个过程。预先感谢任何可能帮我看看的人。

1 个答案:

答案 0 :(得分:0)

好的,所以我终于完成了实现。我不得不交换大量的代码,最后使用JSON完成了包装对象/列表的组合。本质上,问题在于二进制格式化程序将对象反序列化后将其弄乱了。每次我更新对象值时,如果不访问它们,它们将无缘无故地随意改变。

这很令人惊讶,因为我阅读的关于保存的帖子中大约有一半说它是好的,而其他This之类的人则说这是不好的做法。我最初进行了一些研究,但没有发现使用它的负面影响。我仍然遇到问题,但是Json成功地保留了数据,并且我的对象没有弄乱。我很确定这是问题所在,因为我更改的唯一部分是将对象的值结构公开以进行序列化,并将json结构实现到了SaveLoad脚本中。 This视频对于整体结构和入门非常有用,This线程在遇到一些问题时帮助我进行了故障排除。

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

无论如何,希望我的努力和寻找答案可能会有所帮助,因为我发现的事情非常分散且难以理解。