unity保存游戏数据
In this tutorial, we’ll learn to implement Save/Load game functionality in our game. We will begin by saving the necessary player-related data such as the level we’re on, where we are, and our example tutorial statistics.
在本教程中,我们将学习在游戏中实现“保存/加载”游戏功能。 我们将首先保存与玩家相关的必要数据,例如我们所处的级别,所在的位置以及示例教程统计信息。
If you need a project for testing, you can use the one at the end of the previous article which dealt with cross-scene saving of data, and it’s perfect for following this tutorial:
如果您需要一个项目进行测试,则可以使用上一篇文章结尾处的一个项目,该项目涉及跨场景的数据保存,它是遵循本教程的理想选择:
Saving Data Between Scenes in Unity – previous article [GitHub Repository] [ZIP Download]
在Unity中的场景之间保存数据–上一篇文章 [GitHub存储库] [ZIP下载]
If you want to download a finished project, the link is at the end of this article.
如果要下载完成的项目,则该链接位于本文的末尾。
For saving the in-game data to a hard drive in a format that can be understood and loaded later on, we will be using a .NET/Mono feature known as Serialization. You can find out more about theory of serialization at the following links:
为了将游戏中的数据以一种可以理解并稍后加载的格式保存到硬盘驱动器,我们将使用一种称为序列化的.NET / Mono功能。 您可以在以下链接中找到有关序列化理论的更多信息:
MSDN Serialization info
MSDN序列化信息
Binary Serialization example
二进制序列化示例
In short, “serializing” means writing down a .NET object to a hard drive in its raw binary form. It may sound a bit unintuitive, but think of it this way: we’re saving an instance of a class to a hard drive.
简而言之,“序列化”是指将.NET对象以其原始二进制格式写到硬盘中。 听起来可能有点不直观,但是请这样想:我们将类的实例保存到硬盘中。
You may remember that while finishing up our last example, we wrapped our player’s data into a single class. You may already be able to tell where this is going. Let’s just go over the logic flow real quick:
您可能还记得,在完成最后一个示例时,我们将播放器的数据包装到一个类中。 您可能已经能够知道前进的方向。 让我们快速回顾一下逻辑流程:
Saving data:
保存数据:
Get a class containing player’s data 获取一个包含玩家数据的类 Serialize down to hard drive into known file 向下序列化到硬盘驱动器到已知文件Loading data:
加载数据中:
Find known save file 查找已知的保存文件Deserialize the contents into generic object
将内容反序列化为通用对象
Cast object into the type of our data class
将对象转换为数据类的类型
What do we need to save?
我们需要保存什么?
Everything that was already in the PlayerStatistics class which was saved across scenes PlayerStatistics类中已经存在的所有内容,已跨场景保存 Scene ID where the game was saved 保存游戏的场景ID Location in the scene where the player was when the game was saved 保存游戏时玩家所在的场景中的位置Using the project from our previous example, we will need to prepare some things in order to begin writing this functionality correctly. We need to consider the following problems:
使用上一个示例中的项目,我们将需要准备一些东西,以便开始正确地编写此功能。 我们需要考虑以下问题:
What scene was the player on when the game was saved? 保存游戏时,玩家处于哪个场景? Where in the scene was the player? 玩家在场景中的什么地方? How do we initialize the Saving procedure? 我们如何初始化保存程序? How do we initialize the Loading procedure? 我们如何初始化加载程序? How do we know if we need to start the level anew, or load existing data? 我们如何知道是否需要重新开始该级别或加载现有数据?Scene identification? Adding a new Integer variable into our Player’s data package class, so we know what scene the player was on.
场景识别? 在Player的数据包类中添加一个新的Integer变量,这样我们就可以知道播放器所在的场景。
Scene position? Unfortunately, we cannot add a Transform or Vector3 object into the Player’s data package class because they are not serializable objects. We need to add three new float values denoting the player’s X, Y, and Z position instead, and apply them to the position vector of the player when we are loading data.
场景位置? 不幸的是,我们不能将Transform或Vector3对象添加到Player的数据包类中,因为它们不是可序列化的对象。 我们需要添加三个新的浮点值来分别表示玩家的X,Y和Z位置,并在加载数据时将其应用于玩家的位置矢量。
Saving/Loading procedure Keeping it simple for now, we will assign two hotkeys for save and load: F5 and F9 respectively.
保存/加载过程现在保持简单,我们将为保存和加载分配两个热键:分别为F5和F9。
Fresh start or load? We will need to keep a boolean value which tells us if the scene has been loaded, or started anew. For this, we need our GlobalObject class which persists between scenes because we will need to load the data and set the variable, before initializing the loading procedure of the scene the player was on when they saved the game.
重新开始还是加载? 我们将需要保留一个布尔值,该值告诉我们场景是否已加载或重新开始。 为此,我们需要在场景之间保留的GlobalObject类,因为在初始化玩家保存游戏时所在的场景的加载过程之前,我们需要加载数据并设置变量。
This may sound a bit disorienting so let’s break this down into a flowchart.
这听起来有些令人迷惑,所以我们将其分解为流程图。
This explains the flow of the program from a PlayerControl class, which is the primary class we’ll be dealing with today. It’s a class that’s responsible for the player’s input:
这说明了PlayerControl类的程序流程,该类是我们今天要处理的主要类。 这是负责玩家输入的课程:
Notice a few oddities:
请注意一些奇怪的地方:
Global Control now always carries a publicly available bool that states whether we are loading a scene, or starting it anew. It also carries the copy of a saved player’s data. 现在,Global Control始终带有一个公开可用的公告,指出我们是加载场景还是重新创建场景。 它还带有保存的播放器数据的副本。 Player Control class at Starting (Scene Loading event) always checks if maybe the scene is Loaded from a save game, or anew. This tells us if we need to copy the loaded data, or leave it alone. 开始时的“播放器控件”类(“场景加载”事件)始终检查是否从保存游戏加载场景或重新加载场景。 这告诉我们是否需要复制加载的数据,或将其保留。Let’s tackle the new functionality step by step.
让我们逐步解决新功能。
First, we need to solve the two biggest problems as listed above: Scene ID, and position of the player within the scene.
首先,我们需要解决上面列出的两个最大问题:场景ID,以及播放器在场景中的位置。
Assuming we have a class that holds our player’s data, like this:
假设我们有一个保存玩家数据的类,如下所示:
public class PlayerStatistics { public float HP; public float Ammo; public float XP; }We need to add the following:
我们需要添加以下内容:
[Serializable] public class PlayerStatistics { public int SceneID; public float PositionX, PositionY, PositionZ; public float HP; public float Ammo; public float XP; }Let’s get the obvious out of the way first: We have a custom “attribute” declaration in front of the class: [Serializable]. This tells the Engine that the data in this class is suitable for writing down in binary form, or “serializing”.
首先让我们消除明显的障碍:在类的前面有一个自定义的“属性”声明: [Serializable] 。 这告诉引擎,该类中的数据适合以二进制形式记下或“序列化”。
We have also added our Scene ID and Position values.
我们还添加了场景ID和位置值。
Now, let’s write the functions that will serialize and deserialize data. We need to go into our GlobalObject (or similar object you should have):
现在,让我们编写将对数据进行序列化和反序列化的函数。 我们需要进入我们的GlobalObject(或您应该拥有的类似对象):
//In global object: public PlayerStatistics LocalCopyOfData; public bool IsSceneBeingLoaded = false; public void SaveData() { if (!Directory.Exists("Saves")) Directory.CreateDirectory("Saves"); BinaryFormatter formatter = new BinaryFormatter(); FileStream saveFile = File.Create("Saves/save.binary"); LocalCopyOfData = PlayerState.Instance.localPlayerData; formatter.Serialize(saveFile, LocalCopyOfData); saveFile.Close(); } public void LoadData() { BinaryFormatter formatter = new BinaryFormatter(); FileStream saveFile = File.Open("Saves/save.binary", FileMode.Open); LocalCopyOfData = (PlayerStatistics)formatter.Deserialize(saveFile); saveFile.Close(); }OK, this is a lot of code at once, let’s break it down. Let’s first explain the Save function:
好的,这一次有很多代码,让我们分解一下。 让我们首先解释一下保存功能:
Path check
路径检查
if (!Directory.Exists("Saves")) Directory.CreateDirectory("Saves");While File.Create will create a new file, it will not create the path of directories where the file is supposed to be located. Therefore, if the directory Saves does not exist, an exception will be thrown and the game will not be saved.
尽管File.Create将创建一个新文件,但不会创建该文件应位于的目录路径。 因此,如果目录Saves不存在,则会引发异常,并且游戏将不会被保存。
Binary Formatter
二进制格式化程序
BinaryFormatter formatter = new BinaryFormatter();This will require adding a new Using namespace above, specifically this one:
这将需要在上面添加一个新的Using名称空间,特别是以下名称空间:
using System.Runtime.Serialization.Formatters.Binary;Pro-tip: Write “BinaryFormatter” in the code and without pressing Space, Enter or Tab (which will initiate Intellisense completion in both MonoDevelop and Visual Studio), hit Right-click over the declaration and use Resolve -> Add Using namespace.
提示:在代码中编写“ BinaryFormatter”,无需按空格键,Enter或Tab键(将在MonoDevelop和Visual Studio中启动Intellisense完成),在声明上单击鼠标右键,然后使用Resolve-> Add Using namespace 。
File
文件
FileStream saveFile = File.Create("Saves/save.binary");This will require the following namespace: using System.IO;
这将需要以下名称空间: using System.IO;
The Stream object we will have gotten will create a new file if it doesn’t exist, and overwrite it if it does exist, under a path we have set. For the purpose of the example, we’re using a very simple and hardcoded relative path.
如果不存在,我们将获得的Stream对象将在我们设置的路径下创建一个新文件,如果存在则将其覆盖。 出于示例的目的,我们使用非常简单且经过硬编码的相对路径。
Note: You can also use whatever extension of the file you want, or even no extension at all. It doesn’t matter since no Operating System will open this file, nor is there any program out there that will read this file’s format or requires extension association. We can only read the file if we know the source code of the class from which it was serialized.
注意 :您也可以使用所需文件的任何扩展名,甚至完全不使用扩展名。 没关系,因为没有操作系统会打开该文件,也没有任何程序可以读取该文件的格式或需要扩展名关联。 仅当我们知道从中进行序列化的类的源代码时,才能读取该文件。
Data
数据
LocalCopyOfData = PlayerState.Instance.localPlayerData;We need to fetch a reference to the object we are serializing. In my project’s example, all the relevant player data is contained in an instance of the “PlayerStatistics” class, located within the singleton instance of the PlayerState class.
我们需要获取对要序列化的对象的引用。 在我的项目示例中,所有相关的播放器数据都包含在“ PlayerStatistics”类的实例中,该类位于PlayerState类的单例实例内。
The magic
魔术
formatter.Serialize(saveFile, LocalCopyOfData);The above represents the entire difficulty of writing a class in its raw binary form to the hard drive. Ah, the joys of the .NET/Mono framework!
上面的内容代表了将原始二进制格式的类写入硬盘驱动器的全部困难。 啊,.NET / Mono框架的乐趣!
You will notice the Serialize function requires two arguments:
您会注意到Serialize函数需要两个参数:
Stream object. Our FileStream is an extension of a Stream object so we can use that.
流对象。 我们的FileStream是Stream对象的扩展,因此我们可以使用它。
object that will be serialized. As you can see, we can serialize literally everything (as long as it carries the Serializable attribute), because everything within the .NET/Mono framework is extended from the base object class.
将被序列化的对象 。 如您所见,我们可以对所有内容进行序列化(只要它带有Serializable属性即可),因为.NET / Mono框架中的所有内容都是从基础对象类扩展而来的。
Do NOT forget this!
不要忘了这个!
saveFile.Close();Seriously, do not forget this.
认真地说,不要忘记这一点。
If we forget to close the Stream object in our code, we will encounter one of the two problems (whichever happens first):
如果我们忘记在代码中关闭Stream对象,则将遇到以下两个问题之一(以先发生的为准):
Any attempt to access or remove the file on the hard drive (which may or may not appear empty), will give an OS error message saying the file is in use by another program. 任何尝试访问或删除硬盘驱动器上的文件(可能显示为空或可能不显示为空)的操作都会显示OS错误消息,指出该文件正在被另一个程序使用。 Attempting to deserialize an unclosed object will stall the program on the deserialization line, without exceptions or warnings. It’ll simply stop giving signs of life. 尝试对未关闭的对象进行反序列化将使程序在反序列化行上停顿,没有任何异常或警告。 它只会停止提供生命迹象。Note how neither of the symptoms actually gives any meaningful information about an unclosed stream.
请注意,这两种症状实际上都没有给出关于未关闭流的任何有意义的信息。
OK, let’s go look at the Load function:
好,让我们来看一下Load函数:
File (opening)
文件(打开)
FileStream saveFile = File.Open("Saves/save.binary", FileMode.Open);Instead of using Create, we will be using the Open function to obtain our Stream object. Self explanatory, really.
我们将使用Open函数来获取Stream对象,而不是使用Create。 自我解释,真的。
The magic difference
神奇的区别
LocalCopyOfData = (PlayerStatistics)formatter.Deserialize(saveFile);Note how we are not yet feeding the loaded data into our PlayerState instance.
请注意,我们如何尚未将加载的数据馈送到PlayerState实例中。
This is because we first need to load the data to determine what scene the player is on, then we need to load that scene, and then feed the loaded data.
这是因为我们首先需要加载数据以确定播放器所在的场景,然后需要加载该场景, 然后提供已加载的数据。
Finally, let’s implement our Save/Load logic somewhere.
最后,让我们在某处实现“保存/加载”逻辑。
A good place for this example would be the class that handles the player’s input. Within the example project, that would be our PlayerControl class.
此示例的一个好地方是处理玩家输入的类。 在示例项目中,这将是我们的PlayerControl类。
Just for this example, we’ll put the following code directly in our PlayerControl class’ Update function, but as development goes on, we’ll need to move this into the part of the code where the player actually has control (when no menus are opened, cutscene isn’t playing, etc):
就本例而言,我们将以下代码直接放入PlayerControl类的Update函数中,但是随着开发的进行,我们需要将其移至播放器实际具有控制权的代码部分(当没有菜单时)被打开,过场动画没有播放等):
///In Control Update(): if (Input.GetKey(KeyCode.F5)) { PlayerState.Instance.localPlayerData.SceneID = Application.loadedLevel; PlayerState.Instance.localPlayerData.PositionX = transform.position.x; PlayerState.Instance.localPlayerData.PositionY = transform.position.y; PlayerState.Instance.localPlayerData.PositionZ = transform.position.z; GlobalControl.Instance.SaveData(); } if (Input.GetKey(KeyCode.F9)) { GlobalControl.Instance.LoadData(); GlobalControl.Instance.IsSceneBeingLoaded = true; int whichScene = GlobalControl.Instance.LocalCopyOfData.SceneID; Application.LoadLevel(whichScene); }The Quicksave function:
快速保存功能:
Saves the current Scene ID into the current player data 将当前场景ID保存到当前播放器数据中 Saves the current player location into the current player data 将当前玩家位置保存到当前玩家数据中 Calls the function to save the player data into the save file 调用将播放器数据保存到保存文件的功能Now, the Quickload function is a bit different:
现在,Quickload功能有所不同:
First, we use the function to load the data into GlobalControl’s “LocalCopyOfData” instance. After that, we poke it to find which scene the player is saved on.
首先,我们使用函数将数据加载到GlobalControl的“ LocalCopyOfData”实例中。 之后,我们戳它以查找播放器保存在哪个场景上。
We set the public boolean variable that says the scene is now being loaded, and initialize the LoadLevel function.
我们设置表示当前正在加载场景的公共布尔变量,并初始化LoadLevel函数。
You might be wondering: “We aren’t even copying the player position or PlayerStatistics data, so… why are we doing this?”
您可能想知道:“我们甚至没有复制玩家位置或PlayerStatistics数据,所以……我们为什么要这样做?”
If you remember the flowchart from before, in the PlayerControl’s Start function we query the Global Control for this boolean variable, and then copy over the loaded data.
如果您还记得以前的流程图,则在PlayerControl的Start函数中,我们向Global Control查询此布尔变量, 然后复制加载的数据。
This is because we cannot copy over the data and then load the scene. The data will not be carried over. We also cannot load the scene first, and copy the data in the same function, because anything after the LoadLevel() function will be ignored, since the object and the script are destroyed and a new level starts with new objects.
这是因为我们无法复制数据然后加载场景。 数据将不会被保留。 我们也不能先加载场景,并在同一函数中复制数据,因为在LoadLevel()函数之后的所有内容都将被忽略,因为对象和脚本被破坏了,新的关卡从新的对象开始。
So, we use a bit of a workaround – we use the GlobalObject, which persists between those loadings, to load our data into the player.
因此,我们使用了一些解决方法–我们使用在两次加载之间保持不变的GlobalObject将数据加载到播放器中。
In our PlayerControl Start() Function we need:
在PlayerControl Start()函数中,我们需要:
///In Control Start() if (GlobalControl.Instance.IsSceneBeingLoaded) { PlayerState.Instance.localPlayerData = GlobalControl.Instance.LocalCopyOfData; transform.position = new Vector3( GlobalControl.Instance.LocalCopyOfData.PositionX, GlobalControl.Instance.LocalCopyOfData.PositionY, GlobalControl.Instance.LocalCopyOfData.PositionZ + 0.1f); GlobalControl.Instance.IsSceneBeingLoaded = false; }As you can see, we copy over the data first and then move the player to the saved location. We also move the player just a tiny amount upwards from the saved position, just to avoid any physics-related errors. After that we set the control boolean to false.
如您所见,我们首先复制数据,然后将播放器移至保存的位置。 我们还将播放器从保存的位置向上移动了一点,以避免任何与物理相关的错误。 之后,我们将控件布尔值设置为false。
You can now test your in-game saving and loading. Feel free to change the statistics of the player, or traverse to another scene and hit the F5 button. You can now exit the game, restart your computer, it doesn’t matter.
现在,您可以测试游戏中的保存和加载。 随时更改播放器的统计信息,或遍历另一个场景并按F5按钮。 您现在可以退出游戏,重新启动计算机,没关系。
When you turn the game back on, (or are just impatient, you can immediately) hit F9, and continue where you left off!
当您重新打开游戏时(或只是急躁,您可以立即)按F9,然后从上次停止的地方继续!
If you want to get fancy, try to write additional redundancy checks to see if the save file exists or even incrementing save files (so you can always load an earlier save).
如果想花哨的话,请尝试编写其他冗余检查,以查看保存文件是否存在,甚至增加保存文件的数量(以便始终可以加载较早的保存)。
If you want to see how the finished demo works or looks like (or if you got stuck somewhere and need help), the project can be downloaded from the following locations:
如果您想查看完成的演示的工作方式或外观(或者您被困在某个地方并需要帮助),可以从以下位置下载该项目:
Github Repository
Github仓库
Zip file with Unity project
Unity项目的Zip文件
We have saved and loaded the player’s and relevant info, but what about the rest of the in-game world? What if we have pick ups in the world we want to save? Or enemies we want to stay dead?
我们已经保存并加载了玩家的相关信息,但是其余的游戏世界呢? 如果我们要保存的世界上有捡拾物品怎么办? 还是我们想留下来的敌人?
This will be covered in a followup to this tutorial, coming shortly. Until then, if you want to come prepared and armed to the teeth with code, look up .NET/Mono’s Delegates and Events.
不久将在本教程的后续文章中介绍。 在此之前,如果您想做好准备并用代码武装起来,请查阅.NET / Mono的Delegates and Events 。
Questions? Comments? Please leave them below, and don’t forget to hit that thumbs up button if you liked this writeup!
有什么问题吗 注释? 请把它们留在下面,如果您喜欢这篇文章,别忘了点击那个大拇指 !
翻译自: https://www.sitepoint.com/saving-and-loading-player-game-data-in-unity/
unity保存游戏数据