unity保存加载慢
Thanks to Vincent Quarles for kindly helping to peer review this article.
感谢Vincent Quarles慷慨地帮助同行审阅本文。
In this tutorial, we’ll finish the implementation of Save and Load functionality in our game. In the previous tutorial on Saving and Loading Player Game Data in Unity, we successfully saved and loaded player-related data such as statistics and inventory, but now we’ll tackle the most difficult part – world objects. The final system should be reminiscent of The Elder Scrolls games – each and every object saved exactly where it was, for indefinite amount of time.
在本教程中,我们将完成游戏中“保存和加载”功能的实现。 在上一个有关在Unity中保存和加载玩家游戏数据的教程中 ,我们成功保存和加载了与玩家有关的数据,例如统计数据和库存,但现在我们将解决最困难的部分–世界对象。 最终的系统应该让人联想到《上古卷轴》游戏 –每个对象都可以无限期地准确保存在原位置。
If you need a project to practice on, here’s a version of the project we completed in the last tutorial. It has been upgraded with a pair of in-game interactable objects that spawn items – one potion and one sword. They can be spawned and picked up (despawned), and we need to save and load their state correctly. A finished version of the project (with save system fully implemented) can be found at the bottom of this article.
如果您需要进行练习的项目,这是我们在上一个教程中完成的项目的版本。 它已升级为带有一对可生成物品的游戏内可交互对象-一把魔药和一把剑。 可以生成它们并拾取它们( despawned ),我们需要正确保存和加载它们的状态。 您可以在本文底部找到该项目的完成版本(已完全实现保存系统)。
Project GitHub PageProject ZIP Download
项目GitHub页面 项目ZIP下载
We need to break down the system of saving and loading objects before we implement it. First and foremost, we need some sort of Level master object that will spawn and despawn objects. It needs to spawn saved objects in the level (if we are loading the level and not starting anew), despawn picked up objects, notify the objects that they need to save themselves, and manage the lists of objects. Sounds like a lot, so let’s put that into a flowchart:
在实现之前,我们需要分解保存和加载对象的系统。 首先,我们需要某种可以生成和取消生成对象的Level主对象。 它需要在关卡中生成已保存的对象(如果我们正在加载关卡而不是重新开始),则生成拾取的对象,通知对象它们需要保存自己,并管理对象列表。 听起来很多,所以让我们将其放入流程图中:
Basically, the entirety of the logic is saving a list of objects to a hard drive – which, the next time the level is loaded, will be traversed, and all objects from it spawned as we begin playing. Sounds easy, but the devil is in the details: how do we know which objects to save and how do we spawn them back?
基本上,整个逻辑是将对象列表保存到硬盘驱动器–下一次加载关卡时,将遍历该对象,并且在我们开始播放时会生成其中的所有对象。 听起来很简单,但细节在于魔鬼:我们如何知道要保存的对象以及如何将它们生成?
In the previous article, I mentioned we’ll be using a delegate-event system for notifying the objects that they need to save themselves. Let’s first explain what delegates and events are.
在上一篇文章中,我提到过我们将使用委托事件系统来通知对象它们需要保存自己。 让我们首先解释什么是委托和事件 。
You can read the Official Delegate documentation and the Official Events documentation. But don’t worry: even I don’t understand a lot of the technobabble in official documentation, so I’ll put it in plain English:
您可以阅读官方代表文档和官方活动文档 。 但请放心:即使我在官方文档中也不了解很多技术问题,所以我将其用简单的英语表达:
You can think of a delegate as a function blueprint. It describes what a function is supposed to look like: what its return type is and what arguments it accepts. For example:
您可以将委托视为功能蓝图 。 它描述了函数的外观:返回类型是什么以及接受的参数。 例如:
public delegate void SaveDelegate(object sender, EventArgs args);This delegate describes a function that returns nothing (void) and accepts two standard arguments for a .NET/Mono framework: a generic object which represents a sender of the event, and eventual arguments which you can use to pass various data. You don’t really have to worry about this, we can just pass (null, null) as arguments, but they have to be there.
该委托描述了一个不返回任何值(无效)并接受.NET / Mono框架的两个标准参数的函数:一个表示事件发送者的通用对象,以及可用于传递各种数据的最终参数。 您不必真的为此担心,我们可以将(null,null)作为参数传递,但是它们必须存在。
So how does this tie in with events?
那么这如何与事件联系在一起?
You can think of an event as a box of functions. It accepts only functions that match the delegate (a blueprint), and you can put and remove functions from it at runtime however you want.
您可以将事件视为功能盒 。 它仅接受与委托匹配的函数(蓝图),并且可以在运行时根据需要放置和删除函数。
Then, at any time, you can trigger an event, which means run all the functions that are currently in the box – at once. Consider the following event declaration:
然后,您可以随时触发一个事件,这意味着立即运行该框中当前的所有功能。 考虑以下事件声明:
public event SaveDelegate SaveEvent;This syntax says: declare a public event (anybody can subscribe to it – we’ll get to that later), which accepts functions as described by SaveDelegate delegate (see above), and it’s called SaveEvent.
这种语法说:声明一个公共事件(任何人都可以订阅它-我们将在后面进行讨论),该事件接受SaveDelegate委托(见上文)所述的功能,称为SaveEvent 。
Subscribing to an event basically means ‘putting a function in the box’. The syntax is quite simple. Let’s suppose our event is declared in our well-known GlobalObject class and that we have some Potion object class named PotionDroppable that needs to subscribe to an event:
订阅事件基本上意味着“将功能放入盒子”。 语法非常简单。 假设我们的事件是在我们著名的GlobalObject类中声明的,并且我们有一个名为PotionDroppable的 Potion对象类需要订阅一个事件:
//In PotionDroppable's Start or Awake function: GlobalObject.Instance.SaveEvent += SaveFunction; //In PotionDroppable's OnDestroy() function: GlobalObject.Instance.SaveEvent -= SaveFunction; //[...] public void SaveFunction (object sender, EventArgs args) { //Here is code that saves this instance of an object. }Let’s explain the syntax here. We first need to have a function that conforms to the described delegate standards. In the object’s script, there is such a function named SaveFunction. We’ll write our own later, but for now let’s just assume that it’s a working function for saving the object to a hard drive so it can be loaded later.
让我们在这里解释语法。 我们首先需要具有符合所描述委托标准的功能。 在对象的脚本中,有一个名为SaveFunction的函数。 稍后我们将编写自己的代码,但是现在我们仅假定它是用于将对象保存到硬盘驱动器以便稍后加载的有效函数。
When we have that, we’re simply putting that function in the box at the Start or Awake of the script and removing it when it’s destroyed. (Unsubscribing is quite important: if you try to call a function of a destroyed object you’ll get null reference exceptions at runtime). We do this by accessing the declared Event object, and using an addition operator followed by function name.
有了该功能后,我们只需将该功能放在脚本的“开始”或“唤醒”框中,并在销毁脚本时将其删除。 (取消订阅非常重要:如果尝试调用已销毁对象的函数,则在运行时将获得null引用异常)。 我们通过访问声明的Event对象并使用加法运算符和函数名来完成此操作。
Note: We’re not calling a function using brackets or arguments; we’re simply using the function’s name, nothing else.
注意:我们不使用括号或参数来调用函数; 我们只是在使用函数的名称,仅此而已。
So let’s explain what all this ultimately does in some example flow of the game.
因此,让我们在游戏的某些示例流程中解释所有这些最终的作用。
Let’s assume that, through the flow of the game, the player’s actions have spawned two swords and two potions in the world (for example, the player opened a chest with loot).
假设通过游戏流程,玩家的动作在世界上产生了两把剑和两个药水(例如,玩家用战利品打开了箱子)。
These four objects register their functions in the Save event:
这四个对象在Save事件中注册其功能:
Now let’s assume the player picks up one sword and one potion from the world. As the objects are ‘picked up’, they’re effectively triggering a change in the player’s inventory and then destroying themselves (kind of ruins the magic, I know):
现在假设玩家从世界上捡起一把剑和一瓶魔药。 当这些物体被“捡起”时,它们有效地触发了玩家库存的变化,然后摧毁了自己(我知道有点毁灭了魔法):
And then, suppose the player decides to save the game – maybe they really need to answer that phone which rang out three times now (hey, you made a great game):
然后,假设玩家决定保存游戏–也许他们确实需要接听现在响了三声的电话(嘿,您做的很棒的游戏):
Basically, what happens is the functions in the box get triggered one by one, and the game’s not going anywhere until all functions are done. Every object that ‘saves itself’ is basically writing down itself in a list – which, on the next game Load, will be inspected by Level Master object and all objects that are in the list will be Instantiated (spawned). That’s it, really: if you’ve followed the article so far, you’re basically ready to start implementing it right away. Nevertheless, we’re going to some concrete code examples here, and as always, there will be a finished project waiting for you at the end of the article if you wish to see how the whole thing is supposed to look.
基本上,发生的事情是盒子中的功能被一个接一个地触发,直到所有功能都完成,游戏才得以发展。 每个“保存自身”的对象基本上都在一个列表中写下自己的位置-在下一个游戏Load中 ,将由Level Master对象检查该对象,并且列表中的所有对象都将被实例化(派生)。 就是这样:如果您到目前为止已经阅读了本文,则基本上可以立即开始实施它。 不过,我们将在这里给出一些具体的代码示例,并且与往常一样,如果您希望了解整个外观,那么在本文结尾处将有一个完成的项目等着您。
Let’s first go over the existing project and familiarize ourselves with what’s inside already.
首先让我们浏览现有项目,并熟悉其中的内容。
As you can see, we have these beautifully designed boxes which act as spawners for the two objects we already mentioned. There’s a pair in each of the two scenes. You can immediately see the problem: if you transition the scene or use F5/F9 to Save/Load, the spawned objects will disappear.
如您所见,我们有这些设计精美的盒子,它们充当我们已经提到的两个对象的生成器。 两个场景中的每个都有一对。 您可以立即看到问题:如果转换场景或使用F5 / F9保存/加载,则生成的对象将消失。
The box spawners and the spawned objects themselves use a simple interactable interface mechanic which provides us with the ability to recognize a raycast to those objects, write text on screen, and interact with them using the [E] key.
盒子生成器和生成的对象本身使用简单的可交互界面机制,使我们能够识别到这些对象的射线广播,在屏幕上书写文本,并使用[E]键与它们交互。
Not much more is present. Our tasks here are:
没有更多的存在。 我们的任务是:
Make a list of Potion objects 列出药水对象 Make a list of Sword objects 列出Sword对象 Implement a global Save event 实施全局保存事件 Subscribe to the event using a saving function 使用保存功能订阅活动 Implement a Level Master object 实现一个关卡母版对象 Make Level Master spawn all saved objects if we’re loading a game. 如果我们正在加载游戏,则使关卡大师会生成所有已保存的对象。As you can see, this is not exactly as trivial as one might hope such a fundamental function would be. In fact, no existing game engine out there (CryENGINE, UDK, Unreal Engine 4, others) really has a simple Save/Load function implementation ready to go. This is because, as you might imagine, save mechanics are really specific to each game. There’s more to just object classes that usually need saving; it’s world states such as completed/active quests, faction friendliness/hostility, even current weather conditions in some games. It gets quite complex, but with the right foundation of the saving mechanics, it gets easy to simply upgrade it with more functionality.
如您所见,这并不像人们希望的那样简单。 实际上,没有任何现有的游戏引擎(CryENGINE,UDK,虚幻引擎4,其他)真正可以使用简单的保存/加载功能实现。 正如您可能想象的那样,这是因为保存机制确实针对每个游戏。 仅仅需要保存的对象类还有更多。 世界状态,例如完成/进行中的任务,派系友好/敌对状态,甚至某些游戏中当前的天气状况。 它变得相当复杂,但是有了节省机制的正确基础,可以轻松地通过更多功能对其进行升级。
Let’s get started with the easy stuff first – the object lists. Our player’s data is saved and loaded via simple representation of the data in the Serializables class.
让我们从简单的东西开始-对象列表。 我们的播放器数据通过Serializables类中数据的简单表示来保存和加载。
In similar fashion, we need some serializable classes which will represent our objects. To write them, we need to know which properties we need to save – for our Player we had a lot of stuff to save. Luckily, for the objects, you’re rarely going to need more than their world position. In our example, we only need to save the Position of the objects and nothing else.
以类似的方式,我们需要一些可序列化的类来表示我们的对象。 要编写它们,我们需要知道需要保存哪些属性-对于Player,我们要保存很多东西。 幸运的是,对于这些对象,您几乎不需要的只是它们的世界地位。 在我们的示例中,我们只需要保存对象的位置,而无需保存其他任何内容。
To nicely structure our code, we’ll begin with a simple classes at the end of our Serializables:
为了更好地构建代码,我们将在Serializables的末尾以一个简单的类开始:
[Serializable] public class SavedDroppablePotion { public float PositionX, PositionY, PositionZ; } [Serializable] public class SavedDroppableSword { public float PositionX, PositionY, PositionZ; }You may be wondering why we’re not simply using a base class. The answer is that we could, but you never really know when you need to add or change specific item properties that need saving. And besides, this is far easier for code readability.
您可能想知道为什么我们不仅仅使用基类。 答案是我们可以,但是您永远不知道何时需要添加或更改需要保存的特定项目属性。 此外,这对于代码可读性而言要容易得多。
By now, you may have noticed I’m using the term Droppable a lot. This is because we need to differentiate between droppable (spawnable) objects, and Placeable objects, which follow different rules of saving and spawning. We’ll get to that later.
到目前为止,您可能已经注意到我经常使用术语Droppable 。 这是因为我们需要区分可放置(可生成)对象和可放置对象,它们遵循不同的保存和生成规则。 我们稍后再讨论。
Now, unlike Player’s data where we know there’s really only one player at any given time, we can have multiple objects like Potions. We need to make a dynamic list, and denote to which scene does this list belong: we can’t spawn Level2’s objects in Level1. This is simple to do, again in Serializables. Write this below our last class:
现在,与玩家的数据不同,我们知道在给定的时间实际上只有一个玩家,我们可以拥有多个像魔药之类的对象。 我们需要创建一个动态列表,并指明该列表属于哪个场景:我们无法在Level1中生成Level2的对象。 再次在Serializables中,这很容易做到。 在我们上一堂课的下面写下这个:
[Serializable] public class SavedDroppableList { public int SceneID; public List<SavedDroppablePotion> SavedPotions; public List<SavedDroppableSword> SavedSword; public SavedDroppableList(int newSceneID) { this.SceneID = newSceneID; this.SavedPotions = new List<SavedDroppablePotion>(); this.SavedSword = new List<SavedDroppableSword>(); } }The best place to make instances of these lists would be our GlobalControl class:
制作这些列表实例的最佳位置是我们的GlobalControl类:
public List<SavedDroppableList> SavedLists = new List<SavedDroppableList>();Our lists are pretty much good to go for now: we’ll access them from a LevelMaster object later when we need to spawn items, and save/load from from Hard Drive from within GlobalControl, like we already do with player data.
现在,我们的列表非常不错:当我们需要生成项目时,我们将通过LevelMaster对象访问它们,然后像我们已经处理播放器数据一样,从GlobalControl的硬盘驱动器中进行保存/加载。
Ah, finally. Let’s implement the famous Event stuff.
啊,终于。 让我们实现著名的Event东西。
In GlobalControl:
在GlobalControl中:
public delegate void SaveDelegate(object sender, EventArgs args); public static event SaveDelegate SaveEvent;As you can see, we’re making the event a static reference, so it’s more logical and easier to work with later.
如您所见,我们将事件设为静态引用,因此它更加合理,以后使用起来也更容易。
One final note regarding Event implementation: only the class which contains the event declaration can fire an event. Anybody can subscribe to it by accessing GlobalControl.SaveEvent +=..., but only GlobalControl class can fire it using SaveEvent(null, null);. Attempting to use GlobalControl.SaveEvent(null, null); from elsewhere will result in compiler error!
关于事件实现的最后一点说明:只有包含事件声明的类才能触发事件。 任何人都可以通过访问GlobalControl.SaveEvent +=...来订阅它,但是只有GlobalControl类可以使用SaveEvent(null, null);触发它SaveEvent(null, null); 。 尝试使用GlobalControl.SaveEvent(null, null); 从其他地方将导致编译器错误!
And that’s it for the Event implementation! Let’s subscribe some stuff to it!
这就是事件实现! 让我们订阅一些东西吧!
Now that we have our event, our objects need to subscribe to it, or, in other words, start listening to an event and react when it fires.
现在我们有了事件,我们的对象需要订阅它 ,或者换句话说, 开始监听事件并在事件触发时做出React。
We need a function that will run when an event fires – for each object. Let’s head over to PotionDroppable script in Pickups folder. Note: Sword doesn’t have its script set up yet; we’ll make it in a moment!
对于每个对象,我们需要一个在事件触发时运行的函数。 让我们转到Pickups文件夹中的PotionDroppable脚本。 注意:Sword尚未设置脚本。 我们一会儿就可以做到!
In PotionDroppable, add this:
在PotionDroppable中,添加以下内容:
public void Start() { GlobalControl.SaveEvent += SaveFunction; } public void OnDestroy() { GlobalControl.SaveEvent -= SaveFunction; } public void SaveFunction(object sender, EventArgs args) { }We correctly did the subscribe and unsubscribe to an event. Now the question remains, how to save this object in the list, exactly?
我们正确地进行了订阅和取消订阅事件。 现在的问题仍然存在, 如何将这个对象准确地保存在列表中?
We first need to make sure we have a list of objects initialized for the current scene.
我们首先需要确保我们具有针对当前场景初始化的对象列表。
In GlobalControl.cs:
在GlobalControl.cs中:
public void InitializeSceneList() { if (SavedLists == null) { print("Saved lists was null"); SavedLists = new List (); } bool found = false; //We need to find if we already have a list of saved items for this level: for (int i = 0; iThis function needs to be fired once per level. Problem is, our GlobalControl carries through the levels and its Start and Awake functions only fire once. We'll get around that by simply calling this function from our Level Master object which we'll create in a moment.
每个级别需要触发一次此功能。 问题是,我们的GlobalControl会执行这些级别,并且其Start和Awake函数仅触发一次。 我们将通过在稍后创建的Level Master对象中调用此函数来解决此问题。
We are going to need a small helper function to return the current active scene list as well. In GlobalControl.cs:
我们将需要一个小的辅助函数来返回当前活动场景列表。 在GlobalControl.cs中:
public SavedDroppableList GetListForScene() { for (int i = 0; iNow we're sure we always have a list to save our items to. Let's go back to our Potion script:
现在,我们确定我们总是有一个列表将项目保存到其中。 让我们回到Potion脚本:
public void SaveFunction(object sender, EventArgs args) { SavedDroppablePotion potion = new SavedDroppablePotion(); potion.PositionX = transform.position.x; potion.PositionY = transform.position.y; potion.PositionZ = transform.position.z; GlobalControl.Instance.GetListForScene().SavedPotions.Add(potion); }This is where all our syntactic sugar coating really shines. This is very readable, easy to understand and easy to change for your own needs when you need it! In short, we create a new ‘potion’ representation, and save it in the actual list.
这就是我们所有语法糖衣的真正亮点。 这是非常易读,易于理解的工具,可在需要时轻松更改! 简而言之,我们创建一个新的“药水”表示形式,并将其保存在实际列表中。
First, a small bit of preparation. Within our existing project, we have a global variable that tells us if the scene is being loaded. But we don’t have such variable to tell us if the scene is being transitioned by using the door. We expect for all the dropped objects to still be there when we return to the previous room, even if we didn’t save/load our game anywhere in between.
首先,做一点准备。 在我们现有的项目中,我们有一个全局变量,该变量告诉我们是否正在加载场景。 但是我们没有这样的变量来告诉我们是否正在通过使用门转换场景。 我们希望所有放下的物体在返回前一个房间时仍会存在,即使我们没有在两者之间的任何地方保存/加载游戏。
To do that, we need to make small change to Global Control:
为此,我们需要对Global Control进行一些小的更改:
public bool IsSceneBeingTransitioned = false;In TransitionScript:
在TransitionScript中:
public void Interact() { //Assign the transition target location. GlobalControl.Instance.TransitionTarget.position = TargetPlayerLocation.position; //NEW: GlobalControl.Instance.IsSceneBeingTransitioned = true; GlobalControl.Instance.FireSaveEvent(); Application.LoadLevel(TargetedSceneIndex); }We’re ready to make a LevelMaster object that will work normally.
我们准备制作一个可以正常工作的LevelMaster对象。
Now we only need to read the lists and spawn the objects from them when we’re loading a game. This is what the Level Master object will do. Let’s create a new Script and call it LevelMaster:
现在,我们只需要阅读列表并在加载游戏时从列表中生成对象。 这就是Level Master对象将要执行的操作。 让我们创建一个新脚本并将其命名为LevelMaster :
public class LevelMaster : MonoBehaviour { public GameObject PotionPrefab; public GameObject SwordPrefab; void Start () { GlobalControl.Instance.InitializeSceneList(); if (GlobalControl.Instance.IsSceneBeingLoaded || GlobalControl.Instance.IsSceneBeingTransitioned) { SavedDroppableList localList = GlobalControl.Instance.GetListForScene(); if (localList != null) { print("Saved potions count: " + localList.SavedPotions.Count); for (int i = 0; i < localList.SavedPotions.Count; i++) { GameObject spawnedPotion = (GameObject)Instantiate(PotionPrefab); spawnedPotion.transform.position = new Vector3(localList.SavedPotions[i].PositionX, localList.SavedPotions[i].PositionY, localList.SavedPotions[i].PositionZ); } } else print("Local List was null!"); } } }That’s a lot of code, so let’s break it down.
那是很多代码,所以让我们分解一下。
The code runs only at the start, which we use to initialize the saved lists in GlobalControl if needed. Then, we ask the GlobalControl if we’re loading or transitioning a scene. If we’re starting the scene anew (like New Game or such), it doesn’t matter – we spawn no objects.
该代码仅在开始时运行,如果需要,我们可以使用它来初始化GlobalControl中保存的列表。 然后,我们询问GlobalControl是否正在加载或转换场景。 如果我们重新开始场景(例如“新游戏”之类),那就没关系–我们不会产生任何物体。
If we are loading a scene, we need to fetch our local copy of the list of saved objects (just to save a bit of performance on repeated accessing of GlobalControl, and to make syntax more readable).
如果我们加载一个场景,我们需要获取本地保存的对象名单的复印件(只是为了节省一点点性能上重复访问GlobalControl的, 并且使语法更具可读性)。
Next, we simply traverse the list and spawn all potion objects inside. The exact syntax for spawning is basically one of the Instantiate method overloads. We must cast the result of the Instantiate method into GameObject (for some reason, the default return type is simple Object) so that we can access its transform, and change its position.
接下来,我们仅遍历列表并生成其中的所有药水对象。 产生的确切语法基本上是Instantiate方法重载之一。 我们必须将 Instantiate方法的结果强制转换为GameObject (由于某种原因,默认返回类型为simple Object ),以便我们可以访问其转换并更改其位置。
This is where the object is spawned: if you need to change any other values at spawn-time, this is the place to do it.
这是生成对象的地方:如果需要在生成时更改任何其他值,则可以在此处进行操作。
We need to put our level master in each scene and assign the valid Prefabs to it:
我们需要在每个场景中放置关卡大师,并为其分配有效的预制件:
Now we’re only missing one crucial piece: we need to actually fire the event, serialize the Lists down to the hard drive and read from them. We’ll simply do that in our existing Save and Load functions in GlobalControl:
现在,我们只缺少一个关键部分:我们实际上需要触发事件,将列表序列化到硬盘驱动器并从中读取。 我们只需要在GlobalControl中现有的“保存和加载”函数中执行此操作:
public void FireSaveEvent() { GetListForScene().SavedPotions = new List<SavedDroppablePotion>(); GetListForScene().SavedSword = new List<SavedDroppableSword>(); //If we have any functions in the event: if (SaveEvent != null) SaveEvent(null, null); } public void SaveData() { if (!Directory.Exists("Saves")) Directory.CreateDirectory("Saves"); FireSaveEvent(); BinaryFormatter formatter = new BinaryFormatter(); FileStream saveFile = File.Create("Saves/save.binary"); FileStream SaveObjects = File.Create("saves/saveObjects.binary"); LocalCopyOfData = PlayerState.Instance.localPlayerData; formatter.Serialize(saveFile, LocalCopyOfData); formatter.Serialize(SaveObjects, SavedLists); saveFile.Close(); SaveObjects.Close(); print("Saved!"); } public void LoadData() { BinaryFormatter formatter = new BinaryFormatter(); FileStream saveFile = File.Open("Saves/save.binary", FileMode.Open); FileStream saveObjects = File.Open("Saves/saveObjects.binary", FileMode.Open); LocalCopyOfData = (PlayerStatistics)formatter.Deserialize(saveFile); SavedLists = (List<SavedDroppableList>)formatter.Deserialize(saveObjects); saveFile.Close(); saveObjects.Close(); print("Loaded"); }This also appears to be a lot of code, but the majority of that was already there. (If you followed my previous tutorial, you’ll recognize the binary serialization commands; the only new thing here is the FireSaveEvent function, and one additional file that saves our lists. That’s it!
这似乎也有很多代码,但是其中大多数已经存在。 (如果您遵循了上一教程,您将认识到二进制序列化命令;这里唯一的新功能是FireSaveEvent函数,以及一个用于保存列表的附加文件。就是这样!
If you run the project now, the potion objects will be correctly saved and loaded each time do you hit F5 and F9, or walk through the door (and any combination of such).
如果现在运行该项目,则每次单击F5和F9或走进门(以及它们的任意组合)时,药水对象都会正确保存和加载。
However, there’s one more problem to solve: we’re not saving the swords.
但是,还有一个问题要解决:我们没有省力。
This is simply to demonstrate how to add new savable objects to your project once you have similar foundations built.
这只是为了演示一旦建立了相似的基础后如何向项目添加新的可保存对象。
So let’s say you already have a new object-spawning system in place – like we already do with the sword objects. They are currently not interactable much (beyond basic physics), so we need to write a script similar to the Potion one which will enable us to ‘pick up’ a sword and make it save correctly.
因此,假设您已经有了一个新的对象生成系统-就像我们已经对剑对象所做的那样。 它们目前尚不易交互(除了基本物理学之外),因此我们需要编写类似于Potion的脚本,这将使我们能够“捡起”剑并正确保存。
The prefab of the sword that’s currently being spawned can be found in the Assets > Prefabs folder.
当前正在生成的剑的预制件可以在Assets> Prefabs文件夹中找到。
Let’s make it work. Go to Assets > Scripts > Pickups and there you’ll see PotionDroppable script. Next to it, create a new SwordDroppable script:
让它工作。 转到资产>脚本> 提取,然后您会看到PotionDroppable脚本。 在它旁边,创建一个新的SwordDroppable脚本:
public class SwordDroppable : MonoBehaviour, IInteractable { public void Start() { GlobalControl.SaveEvent += SaveFunction; } public void OnDestroy() { GlobalControl.SaveEvent -= SaveFunction; } public void SaveFunction(object sender, EventArgs args) { SavedDroppableSword sword = new SavedDroppableSword(); sword.PositionX = transform.position.x; sword.PositionY = transform.position.y; sword.PositionZ = transform.position.z; GlobalControl.Instance.GetListForScene().SavedSword.Add(sword); } public void Interact() { Destroy(gameObject); } public void LookAt() { HUDScript.AimedObjectString = "Pick up: Sword"; } }Do not forget the ‘Interactable’ interface implementation. It’s very important: without it your sword will not be recognized by the camera raycast and will remain uninteractable. Also, double check that the Sword prefab belongs to Items layer. Otherwise, it will again be ignored by the raycast. Now add this script to the Sword prefab’s first child (which actually has the Mesh renderer and other components):
不要忘记“交互”界面的实现。 这非常重要:没有它,您的剑将不会被相机射线束识别,并且将保持不可交互性。 另外,再次检查Sword预制件是否属于Items层。 否则,它将再次被光线投射忽略。 现在,将此脚本添加到Sword预制的第一个孩子(实际上有Mesh渲染器和其他组件)中:
Now, we need to spawn them. In Level Master, under our for loop that spawns the Potions:
现在,我们需要生成它们。 在Level Master中,在产生药水的for循环下:
for (int i = 0; i < localList.SavedSword.Count; i++) { GameObject spawnedSword = (GameObject)Instantiate(SwordPrefab); spawnedSword.transform.position = new Vector3(localList.SavedSword[i].PositionX, localList.SavedSword[i].PositionY, localList.SavedSword[i].PositionZ); }… and that’s it. Whenever you need a new item type saved:
…就是这样。 每当您需要保存新的项目类型时:
add in Serializables class 添加Serializables类 create script for item that subscribes to Save event 为订阅Save事件的项目创建脚本 add Instantiate logic in Level Master. 在Level Master中添加实例化逻辑。For now, the system is quite crude: there are multiple save files on the hard drive, the object’s rotation isn’t saved (swords, player, etc.), and logic for positioning a player during scene transitions (but not loading) is a bit quirky.
目前,该系统还很粗糙:硬盘驱动器上有多个保存文件,没有保存对象的旋转(剑,播放器等),并且在场景转换(但未加载)期间放置播放器的逻辑是有点古怪。
These are all now minor problems to solve once the solid system is in place, and I invite you to try to tinker around with this tutorial and finished project to see if you can improve the system.
现在,一旦建立了可靠的系统,这些都是要解决的小问题。我邀请您尝试修改本教程和完成的项目,以查看是否可以改进系统。
But even as it is, it’s already quite a reliable and solid method for doing Save/Load mechanics in your game – however much it may differ from these example projects.
但是尽管如此,它已经是一种在游戏中执行“保存/加载”机制的可靠且可靠的方法-但是与这些示例项目可能有很多不同。
As promised, here is the finished project, should you need it for reference, or because you got stuck somewhere. The save system is implemented as per this tutorial’s instructions and with same naming scheme.
如所承诺的,这是完成的项目,是您需要参考时还是因为卡在某个地方而已。 按照本教程的说明并使用相同的命名方案来实现保存系统。
Project GitHub PageProject ZIP Download
项目GitHub页面 项目ZIP下载
翻译自: https://www.sitepoint.com/mastering-save-and-load-functionality-in-unity-5/
unity保存加载慢