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



更新:此问题可能是由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);
}
}

正如你所看到的,我会验证它是否存在,然后检查我是否正在保存或加载。save函数用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次保存第一个循环开始时输出"第一个循环播放器15"共16次然后它显示16,所以最后一个是正确的,尽管我猜是一个错误。不出所料,第二个循环和第一个循环做得一样。

我在中留下了对updateNames的调用,但注释掉了行并运行了它,包括去掉bump。它再次以17次保存和玩家15的16次迭代开始,但这一次,在最后一次显示"临时名称"时,我只在当前玩家的场景管理脚本开始时定义了一次,它本不应该被保存,即使它已经被我的名称循环覆盖,至少这是我的意图。

场景管理

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

我在这里的时候也是我的游戏经理,尽管我一点也没搞砸。大多数情况下,将其用作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运行了第三个循环,看看调用的类型是否有影响。

即使更改名称,然后立即调用循环进行检查,也会显示值正在更改。我认为这可能与我如何存储这些对象有关,但我不确定这个问题是如何产生的。它不会变为null,而且每次名称都会变为相同,所以它不是完全随机的。在我发现这个问题后,我在这里发布了这个消息,希望很快我能解决它,但也希望如果我不这样做,其他人可能会发现一些东西。我有接下来的3个小时的时间来做这件事,所以我会一直试着时不时地回来看看。提前感谢任何可能为我看一眼的人。

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

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

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

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

最新更新