首页 > 科技周边 > IT业界 > 掌握Unity中的保存和加载功能5

掌握Unity中的保存和加载功能5

Joseph Gordon-Levitt
发布: 2025-02-19 09:21:10
原创
391 人浏览过

掌握Unity中的保存和加载功能5

感谢Vincent Quarle的友好访问本文。 在本教程中,我们将完成游戏中保存和加载功能的实现。在上一个关于“团结”中保存和加载玩家游戏数据的教程中,我们成功保存了与玩家相关的数据,例如统计和库存,但是现在我们将解决最困难的部分 - 世界对象。最终系统应该让人联想到上古卷轴游戏 - 每个物体准确地保存在哪里,无限期的时间。

> 如果您需要一个项目才能练习,请以下是我们在上一个教程中完成的项目的版本。它已经使用了一对源自产生的物品的游戏内相互作用的对象进行升级 - 一把药水和一把剑。它们可以产生并

拾取

>( despawned

),我们需要正确保存和加载其状态。该项目的完成版本(已完全实施了保存系统)可以在本文的底部找到。

下载项目启动文件 >项目github页面 项目zip下载

钥匙要点


>利用委托事件系统来通知对象何时保存其状态,增强保存/加载系统的模块化和灵活性。

>实现一个级别的主对象来管理对象的产卵和绝望,以确保世界对象在游戏过程中保持其状态。

>使用可序列化类来保存对象属性(例如位置),可以轻松地扩展或修改针对不同类型的对象。>

确保正确订阅和取消订阅以将事件保存在对象脚本中以避免错误并有效地管理游戏状态。
    开发一个可靠的系统,用于使用二进制格式来保存和加载,以处理复杂的数据结构并确保数据完整性。
  • 测试并扩展系统以包括新的对象类型和功能,证明了系统的适应性和针对不同游戏开发需求的可伸缩性。
  • 实施理论
  • >我们需要在实现该对象之前分解保存和加载对象的系统。首先,我们需要某种将产生和绝望对象的对象
  • 级别的主体。它需要在级别中保存对象(如果我们正在加载级别而不是重新启动),Despawn拾取对象,通知对象需要保存自己并管理对象列表。听起来很多,所以让我们将其放入流程图中:
  • >基本上,整个逻辑都将对象列表保存到硬盘驱动器中 - 下次加载级别时,将在我们开始播放时产生的所有对象。听起来很容易,但是魔鬼在细节中:我们如何知道要保存哪些对象,以及我们如何将它们产生回去?

    >

    >委托和事件

    >在上一篇文章中,我提到我们将使用委托事件系统通知对象需要保存自己的对象。让我们首先解释什么是代表和事件。

    >

    >您可以阅读官方代表文档和官方事件文档。但请放心:即使我也不了解官方文档中的很多技术,所以我会用简单的英语说:>

    委托

    >您可以将委托人视为函数蓝图。它描述了函数应该是什么样的:它的返回类型是什么以及它接受的参数。例如:

    public delegate void SaveDelegate(object sender, EventArgs args);
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    这个委托描述了一个函数,该函数什么都没有返回(void),并接受.NET/MONO框架的两个标准参数:代表事件发送者的通用对象,以及最终可以使用的参数,您可以使用这些数据传递各种数据。您真的不必担心这一点,我们可以将(Null,Null)作为争论,但必须在那里。

    >这与事件如何结合?

    >

    >事件

    >您可以将事件视为。它仅接受与委托(蓝图)匹配的函数,并且您可以在运行时删除并删除函数。 然后,您可以随时触发事件,这意味着运行当前框中的所有功能 - 立即运行。考虑以下事件声明: 此语法说:声明公共事件(任何人都可以订阅它 - 我们稍后会订阅),该事件接受了Savedelegate委托所描述的功能(请参见上文),并称为SaveEvent。

    >订阅并取消订阅

    public event SaveDelegate SaveEvent;
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    订阅事件基本上意味着“将功能放在框中”。语法很简单。让我们假设我们的事件在我们著名的全球视频类别中声明,并且我们有一些名为

    > potiondroppobable

    的药水对象类,需要订阅事件:>

    >让我们在这里解释语法。我们首先需要具有符合描述的代表标准的函数。在对象的脚本中,有一个名为SaveFunction的函数。我们稍后会写自己的书,但是现在我们假设它是将对象保存到硬盘驱动器中的工作功能,以便以后加载。

    >

    >当我们拥有它时,我们只是在脚本开始或清醒时将该功能放在框中,然后在被销毁时将其删除。 (取消订阅非常重要:如果您尝试调用被破坏对象的函数,则在运行时会得到null引用异常)。我们通过访问已声明的事件对象,然后使用添加操作员然后使用功能名称来做到这一点。

    >注意:我们没有使用括号或参数调用函数;我们只是使用该函数的名称,别无其他。

    >

    因此,让我们解释一下这一切最终在游戏的某个示例流中的作用。>

    >逻辑流

    >假设,通过游戏的流动,玩家的动作催生了世界上的两把剑和两个药水(例如,玩家用战利品打开了一个箱子)。

    >

    这四个对象在保存事件中注册其功能:

    掌握Unity中的保存和加载功能5

    现在,假设玩家从世界上捡起一把剑和一把药水。由于对象被“拾取”,因此它们有效地触发了玩家库存的变化,然后破坏了自己(有点毁了魔术,我知道):>

    掌握Unity中的保存和加载功能5>然后,假设玩家决定保存游戏 - 也许他们确实需要回答现在响了三遍的手机(嘿,您做了一个很棒的游戏):

    >

    掌握Unity中的保存和加载功能5基本上,发生的事情是框中的功能一个一个一个一个一个一个一个触发的功能,并且在所有功能完成之前,游戏才能运行到任何地方。每个“保存自身”的对象基本上都在列表中写下自己 - 在下一个游戏负载上,将由级别的主对象进行检查,列表中的所有对象都将进行实例化(产生)。就是这样:如果您到目前为止遵循了这篇文章,那么您基本上就可以立即开始实施它。尽管如此,我们将在这里进行一些具体的代码示例,一如既往,如果您想看看整个事物应该如何看起来。

    >代码

    >让我们首先介绍现有的项目,并熟悉内部的内容。

    >

    如您所见,我们拥有这些设计精美的盒子,它们充当我们已经提到的两个对象的产卵器。这两个场景中的每个场景都有一对。您可以立即看到问题:如果您过渡场景,或使用

    f5/f9掌握Unity中的保存和加载功能5来保存/加载,则产生的对象将消失。

    盒子产卵器和产卵的对象本身使用了一个简单的可交互接口机械师,该机制使我们能够识别到这些对象,在屏幕上写入文本,然后使用[e]键与它们进行交互。

    没有更多的存在。我们在这里的任务是:

    • 列出药水
    • 的列表
    • 列出剑对象的列表
    • 实施全局保存事件
    • >使用保存功能订阅事件
    • 实现级别主对象
    • 如果我们加载了游戏,则
    • 使级别主产生所有保存的对象。
    • >
    如您所见,这并不像人们希望这样的基本功能那样微不足道。实际上,没有现有的游戏引擎(Cryengine,UDK,Unreal Engine 4,其他)确实可以使用简单的保存/加载功能实现。这是因为,正如您可能想象的那样,节省机制确实是每个游戏的特定特定的。还有更多通常需要保存的对象类;它是世界各州,例如完成/积极的任务,派系友善/敌意,甚至在某些游戏中的当前天气状况。它变得非常复杂,但是有了正确的节省机制的基础,可以简单地使用更多功能升级它。

    >

    >对象类列表

    >让我们首先开始使用简单的内容 - 对象列表。通过简单表示序列化类中的数据来保存和加载我们的播放器数据。

    掌握Unity中的保存和加载功能5>以类似的方式,我们需要一些代表我们对象的可序列化类。要编写它们,我们需要知道我们需要保存的属性 - 对于我们的玩家,我们有很多东西要保存。幸运的是,对于对象,您很少需要比他们的世界地位更多。在我们的示例中,我们只需要保存对象的位置,而无需保存。

    为了很好地构建我们的代码,我们将从一个简单的类开始,从我们的序列化末尾开始:

    >

    >您可能想知道为什么我们不简单地使用基类。答案是我们可以,但是您永远不知道何时需要添加或更改需要保存的特定项目属性。此外,对于代码可读性而言,这要容易得多。
    public delegate void SaveDelegate(object sender, EventArgs args);
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    >

    到目前为止,您可能已经注意到我使用了很多词> droppable

    。这是因为我们需要区分可辍学的(产生)对象和可放置的对象,这些对象遵循不同的保存和产卵规则。我们稍后再解决。

    > 现在,与玩家的数据不同,我们知道在任何给定时间实际上只有一个玩家,我们可以拥有多个诸如药水之类的对象。我们需要列出一个动态列表,并表示此列表所属的场景:我们不能在Level1中spawn Level2的对象。这很容易做到,再次在序列化中。在我们的最后一堂课下面写这篇文章:

    public delegate void SaveDelegate(object sender, EventArgs args);
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    列出这些列表实例的最佳场所将是我们的全球控制类别:

    public event SaveDelegate SaveEvent;
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    >现在我们的列表非常好:稍后我们将在需要产卵项目时从LevelMaster对象访问它们,并从GlobalControl内部的硬盘驱动器中保存/加载,就像我们已经使用Player Data一样。

    >委托和事件

    啊,终于。让我们实施著名的活动。

    在GlobalControl中

    >

    //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.
    }
    
    登录后复制
    登录后复制
    如您所见,我们正在使活动成为静态参考,因此以后更逻辑且更易于工作。

    >

    >有关事件实施的最后一个说明:只有包含事件声明的类才能解雇事件。任何人都可以通过访问globalcontrol.saveevent = ...来订阅它,但是只有GlobalControl类才能使用SaveEvent(null,null)触发它。尝试使用globalcontrol.saveevent(null,null);从其他地方导致编译器错误!

    >

    就是事件实施!让我们订阅一些东西!

    >

    >事件订阅

    >现在我们已经有事件,我们的对象需要订阅它,换句话说,

    开始聆听在触发事件时进行反应。>

    我们需要一个在事件触发时运行的函数 - 对于每个对象。让我们在

    pickups>文件夹中转到PotionDroppoppable脚本。注意:Sword还没有设置其脚本;我们将稍后完成!>

    在PotionDroppable中,添加以下内容:

    我们正确地进行了订阅并取消订阅活动。现在,问题仍然存在,
    [Serializable]
    public class SavedDroppablePotion
    {
        public float PositionX, PositionY, PositionZ;
    }
    
    [Serializable]
    public class SavedDroppableSword
    {
        public float PositionX, PositionY, PositionZ;
    }
    登录后复制
    登录后复制
    如何将此对象保存在列表中?> 我们首先需要确保我们有针对当前场景初始化的对象列表。 在GlobalControl.cs中: 此功能需要每个级别触发一次。问题是,我们的全球控制范围及其开始和清醒功能仅发射一次。我们将通过简单地从我们的级别主对象调用此函数来解决这个问题,我们将在稍后创建。 我们将需要一个小的辅助功能来返回当前的活动场景列表。在GlobalControl.cs中:
    [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>();
        }
    }
    登录后复制
    现在,我们确定我们始终有一个列表可以保存我们的物品。让我们回到我们的药水脚本:
        public List<SavedDroppableList> SavedLists = new List<SavedDroppableList>();
    
    登录后复制
        public delegate void SaveDelegate(object sender, EventArgs args);
        public static event SaveDelegate SaveEvent;
    
    登录后复制
    这是我们所有句法糖涂层真正发光的地方。当您需要时,这是非常可读,易于理解且易于改变的需求!简而言之,我们创建了一个新的“药水”表示,并将其保存在实际列表中。

    创建一个级别的主对象

    首先,一小部分准备工作。在我们现有的项目中,我们有一个全局变量,可以告诉我们是否正在加载场景。但是,我们没有这样的变量来告诉我们是否使用门过渡。我们希望当我们返回上一个房间时,所有掉落的对象仍然存在,即使我们没有在两者之间的任何位置保存/加载游戏。 为此

    在TransitionScript中

    >

    public delegate void SaveDelegate(object sender, EventArgs args);
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    我们已经准备好制作一个正常工作的levelmaster对象。

    >
    public event SaveDelegate SaveEvent;
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    >现在,我们只需要读取列表并在加载游戏时从它们中产生对象。这就是级别主体对象所做的。让我们创建一个新脚本,并将其称为

    levelmaster

    >

    这是很多代码,所以让我们将其分解。>

    >代码仅在开始时运行,如果需要,我们用来在GlobalControl中初始化保存的列表。然后,我们询问GlobalControl是否正在加载或过渡场景。如果我们重新启动场景(例如新游戏或类似的游戏),那没关系 - 我们没有产生任何物体。
    //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.
    }
    
    登录后复制
    登录后复制
    >

    如果我们

    加载场景,我们需要获取我们的本地副本保存的对象列表(只是为了在重复访问GlobalControl,>和的重复访问时节省一些性能使语法更可读)。

    接下来,我们只需遍历列表,并在其中催生所有药水对象。产卵的确切语法基本上是实例化方法过载之一。我们必须将实例化方法的结果投入到> gameObject(由于某种原因,默认返回类型是简单的对象),以便我们可以访问其转换并更改其位置。 >这是对象产生的地方:如果您需要在产卵时间更改任何其他值,那么这是这样做的地方。>

    我们需要将我们的级别主体放在每个场景中,并为其分配有效的预制:>

    >现在我们只缺少一个关键作品:我们需要实际启动活动,将列表序列化为硬盘驱动器并从中读取。我们将在GlobalControl中的现有保存和加载功能中简单地做到这一点:

    这似乎也有很多代码,但是其中大多数已经存在。 (如果您遵循我以前的教程,您将识别二进制序列化命令;这里唯一的新事物是FiresaveEvent函数,还有一个保存我们列表的附加文件。就是这样!

    >

    初始测试掌握Unity中的保存和加载功能5

    如果您现在运行项目,则每次点击

    > f5

    and f9
    [Serializable]
    public class SavedDroppablePotion
    {
        public float PositionX, PositionY, PositionZ;
    }
    
    [Serializable]
    public class SavedDroppableSword
    {
        public float PositionX, PositionY, PositionZ;
    }
    登录后复制
    登录后复制
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>这样)。

    但是,还有一个问题要解决:我们没有保存剑。

    这仅仅是为了演示如何在建立类似基础的基础后向项目中添加新的可保存的对象。

    扩展系统

    >因此,假设您已经有了一个新的对象覆盖系统,就像我们已经对剑对象所做的一样。它们目前的交互不多(基本物理学之外),因此我们需要编写一个类似于药水的脚本,这将使我们能够“捡起”剑并使其正确保存。

    >当前要产生的剑的预制可以在资产> prefabs文件夹中找到。

    >让它起作用。转到资产>脚本>拾音器,在那里您会看到potiondroppable脚本。旁边,创建一个新的SwordDroppoppable脚本:

    public delegate void SaveDelegate(object sender, EventArgs args);
    登录后复制
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    不要忘记“可相互作用”的接口实现。非常重要的是:如果没有它,您的剑将不会被摄像机射线广播所识别,并且将保持不可分割效果。另外,请仔细检查剑前是否属于项目层。否则,Raycast将再次忽略它。现在,将此脚本添加到剑前的第一个孩子(实际上具有网状渲染器和其他组件):

    >

    掌握Unity中的保存和加载功能5

    现在,我们需要产生它们。在级别的大师中,在我们的循环下产生了药水:

    public event SaveDelegate SaveEvent;
    登录后复制
    登录后复制
    登录后复制
    登录后复制

    …就是这样。每当您需要保存的新项目类型时:

    • 添加Serializables类
    • 创建订阅以保存事件
    • 的项目脚本
    • 在级别主持人中添加实例化逻辑。
    • >

    结论

    >目前,系统很粗糙:硬盘上有多个保存文件,对象的旋转未保存(剑,玩家等),并且在场景过渡期间定位播放器的逻辑(但没有)加载)有点古怪。

    一旦实心系统到位,这些现在都是要解决的小问题,我邀请您尝试修改本教程和完成的项目,以查看您是否可以改善系统。

    ,但即使如此,它已经是一种在游戏中进行保存/加载机制的可靠且坚实的方法 - 但是,它可能与这些

    >示例项目有很大不同。 如所承诺的,这是完成的项目,如果您需要参考,或者是因为您被卡在某个地方。根据本教程的说明和相同的命名计划,将实现保存系统。

    下载完成的项目:

    Project GitHub页面
    项目zip下载

    >在Unity中掌握保存和加载功能的常见问题(常见问题解答)5

    >在Unity 5中实现保存系统的最佳方法是什么? PlayerPrefs是一种在游戏会话之间存储和检索数据的简单方法。它使您可以以整数,浮点和字符串的形式保存和加载数据。但是,重要的是要注意,PlayerPrefs不安全,不应用于敏感数据。对于更复杂或安全的数据,您可能需要考虑使用二进制格式或JSON Serialializer。在Unity 5中,可以使用PlayerPrefs,JSON序列化或二进制格式来实现。 PlayerPrefs是最简单的方法,允许您保存和加载整数,浮点和字符串。 JSON序列化更为复杂,但可以提高灵活性和安全性。二进制格式是最安全的方法,但也是最复杂的方法。

    >执行顺序如何影响Unity 5?>

    >如何在Unity 5?

    中保护保存数据5可以通过使用二进制格式或加密来实现。二进制格式将您的数据转换为不容易读取的二进制格式。加密通过以一种只能用特定键来解码的方式来编码您的数据来添加额外的安全性。

    >

    >在Unity 5中使用PlayerPrefs用于保存和加载功能的局限性是什么? 🎜>虽然PlayerPrefs是在Unity 5中实现和加载功能的一种简单便捷的方法,但它具有多个限制。首先,它仅支持整数,浮子和字符串。其次,它不安全,很容易被操纵。最后,playerPrefs具有尺寸限制,这对于具有大量数据的游戏可能是一个问题。

    >我如何保存和加载Unity 5?

    保存和加载中的复杂数据结构使用JSON序列化或二进制格式可以实现Unity 5中的复杂数据结构。 JSON序列化使您可以将复杂的数据结构转换为可以轻松保存和加载的字符串格式。二进制格式是一种更安全的方法,可将您的数据转换为二进制格式。

    >我可以在Unity 5中的不同平台上保存和加载数据吗?跨平台兼容性。例如,playerpRefs是跨平台兼容的,但是二进制格式可能并非在所有平台上兼容。

    >

    我如何在Unity 5?

    中加载问题的问题对问题进行故障排除。可以通过检查脚本的执行顺序,确保您的数据正确序列化或格式化并进行测试,从而在Unity 5中保存和加载功能。您打算在平台上发布的游戏。可以通过最大程度地减少保存和加载的数据量,使用有效的数据结构,并确保您的脚本被良好地实现。

    如何实现Unity中的AutoSave功能5?

    可以通过创建一个脚本来自动保存游戏的脚本,以定期或在特定事件中自动保存您的游戏,可以在Unity 5中实现AutoSave功能。该脚本应使用相同的方法来保存数据与您的手动保存系统。

    >

以上是掌握Unity中的保存和加载功能5的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板