本文将为大家分享如何在Unity中实现简单的游戏存档系统,其中不会包含太多实际的代码,仅介绍在制作过程中需要考虑与解决的问题。该系统由一个学生团队为他们的首款商业游戏《Spirit》的原型开发,虽然简单,但其功能却很完善,也很容易扩展到需要保存状态的游戏中使用。

091922kw01601iv6wx70vw.png

Unity提供了PlayerPrefs来持久化保存游戏状态与数据,但不支持序列化Transform。C#也有大量的文件IO操作选项,但没有提供自动识别对象的方法。下面为大家介绍如何自定义永久性数据存储系统,首先编写一些简单的可序列化的数据结构来存储基本信息(如Transform等),将每个对象与其ID关联,然后将数据写入文件以供再次读取来恢复游戏状态。

第一步是选择生成对象ID的方法,基于创建时间生成的Hash值就很适合用作ID,但C#已经提供了GUID类可用于生成唯一的字符串:

[C#] 纯文本查看 复制代码id = System.Guid.NewGuid().ToString();

下面需要创建一个全局管理器类,来将ID与对应的对象进行关联。这个管理器就是一个单例类,带有几个用于查询键值的容器(C#中可能使用Dictionary结构)。还可以根据实际项目需求在Awake函数中添加一些基本的初始化与设置操作。如果有需要,还可以更新脚本执行顺序以确保管理器要先于ID生成脚本执行。另外,最好添加两个查找函数,一个用于将ID映射到实例ID(用于检查重复),另一个用于将ID直接映射到GameObject(用于加载文件时根据ID查找对象)。

ID脚本需要在对象创建时自动生成ID,并在运行期间保持不变。编写脚本时要注意以下几个关键点:
将ID定义为公共的String类型变了,以便Unity在保存并重新加载场景时,ID可以与对象一起被永久保存。可以利用Unity属性在检视视图中以只读方式显示变量值,以避免被误操作。 如果ID为空或者无效,需要在Awake函数中生成ID并在管理器中注册此ID。 在OnDestroy函数中从管理器中注销,以免后续遍历列表时出现空指针。


可以通过检测对象的实例ID与管理器中“记录”的自定义ID是否一致,来判断键值是否“有效”,以避免出现重复。如果不一致,就重新生成自定义ID。为什么这样呢?如果用ID实例化预制件,或是复制一个带有ID的GameObject,Unity也会复制原有的唯一ID,并且无法在Awake函数或地方分辨是否出现重复ID。而如果设置得当,管理器会在文件中记录ID,所以只要在查询函数中查看键值是否有效,就可以解决该问题。

最后,为ID脚本及管理器脚本添加[ExecuteInEditMode]标签,确保在编辑场景时脚本能正常运行。

正确设置好管理器与ID脚本后,只需简单地将ID脚本添加到任意需要识别并加载数据的对象上即可。为了节省空间,建议只为需要动态恢复的对象添加该脚本。

092507b6u4ddzc60r4h2mm.png

上图使用OGID脚本为对象设置了唯一的ID,Saveable脚本是另一个仅用作保存数据的脚本,它利用[System.Serializable]属性在内部构建数据结构来保存加载对象状态所需的数据,例如位置与方向等。

存储系统的部分就比较简单了,使用内置的路径变量与C#的文件函数,就可以轻松实现管理器。保存和加载数据是整个系统中最简单的部分,创建一些自定义类来存储对象的变换信息及数据,添加函数从GameObject读取数据或写入数据到GameObject,然后将脚本绑定到任意需要存档的对象上。此时可以选择使用自定义文件格式存储数据或直接使用[System.Serializable]属性进行标记,然后新建可序列化类包含自定义容器数组,在存储函数中复制数据容器的引用,使用下面的代码将数据保存到文件中:
[C#] 纯文本查看 复制代码System.IO.Stream s = new System.IO.FileStream(filepath, System.IO.FileMode.Create, System.IO.FileAccess.Write); System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); formatter.Serialize(s, saveGame); s.Close();

可以选择二进制或XML序列化对数据进行编码。但二者都有自己的长短处:
二进制:文本文件不易阅读与编辑(对开发者不友好但能够预防玩家作弊),读写速度略快,相比之下数据较易损坏。 XML:文本文件便于阅读与编辑,读写速度稍慢,文件出错时较易修复。


本例中采用的是二进制文件,因为它可以将对象保存为二进制字符串,或将二进制字符串还原为对象。

System.IO.Stream s = new System.IO.FileStream(filepath, System.IO.FileMode.Open, System.IO.FileAccess.Read); System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); SaveGame saveGame = (SaveGame) formatter.Deserialize(s); s.Close();


对保存的文件进行反序列化后,可以调用自定义函数将文件中的数据还原为GameObject(通过ID查找)。如果构建文件时遍历带有ID的对象,新对象会被自动保存到文件中。还可以修改代码来保存数据类型或调整文件管理器。

092500etrxnt1wpkm66r6w.png

使用Unity自带的UGUI可以很方便的实现游戏截图、调整UI布局并创建存档菜单,并从存档中恢复游戏数据。这里存档信息显示了玩家名称与时间戳,以及玩家存档时的游戏截图。

结语

本文介绍的只是Unity中实现文件存档的其中一点内容,文中的实现方式不一定适用于所有游戏,但其中介绍的思路与关键注意事项可以供大部分游戏参考。希望本文对大家有帮助,我们还会分享一些实际的游戏开发经验,请保持关注!

原文链接:http://www.gamasutra.com/blogs/SamanthaStahlke/20170621/300187/Building_a_Simple_System_for_Persistence_with_Unity.php
感谢Unity官方翻译组成员“rtz43”对本文的贡献(如何加入翻译组)
转载请注明:来自Unity官方中文社区(forum.china.unity3d.com)
Unity, 游戏存档锐亚教育

锐亚教育 锐亚科技 unity unity教程