Unity教程 | 自制简易的游戏存档系统
本文将为大家分享如何在Unity中实现简单的游戏存档系统,其中不会包含太多实际的代码,仅介绍在制作过程中需要考虑与解决的问题。该系统由一个学生团队为他们的首款商业游戏《Spirit》的原型开发,虽然简单,但其功能却很完善,也很容易扩展到其它需要保存状态的游戏中使用。
Unity提供了PlayerPrefs来持久化保存游戏状态与数据,但不支持序列化Transform。C#也有大量的文件IO操作选项,但没有提供自动识别对象的方法。下面为大家介绍如何自定义永久性数据存储系统,首先编写一些简单的可序列化的数据结构来存储基本信息(如Transform等),将每个对象与其ID关联,然后将数据写入文件以供再次读取来恢复游戏状态。
第一步是选择生成对象ID的方法,基于创建时间生成的Hash值就很适合用作ID,但C#已经提供了GUID类可用于生成唯一的字符串:
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脚本添加到任意需要识别并加载数据的对象上即可。为了节省空间,建议只为需要动态恢复的对象添加该脚本。
上图使用OGID脚本为对象设置了唯一的ID,Saveable脚本是另一个仅用作保存数据的脚本,它利用[System.Serializable]属性在内部构建数据结构来保存加载对象状态所需的数据,例如位置与方向等。
存储系统的其它部分就比较简单了,使用内置的路径变量与C#的文件函数,就可以轻松实现管理器。保存和加载数据是整个系统中最简单的部分,创建一些自定义类来存储对象的变换信息及其它数据,添加函数从GameObject读取数据或写入数据到GameObject,然后将脚本绑定到任意需要存档的对象上。此时可以选择使用自定义文件格式存储数据或直接使用[System.Serializable]属性进行标记,然后新建可序列化类包含自定义容器数组,在存储函数中复制数据容器的引用,使用下面的代码将数据保存到文件中:
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的对象,新对象会被自动保存到文件中。还可以修改代码来保存其它数据类型或调整文件管理器。
使用Unity自带的UGUI可以很方便的实现游戏截图、调整UI布局并创建存档菜单,并从存档中恢复游戏数据。这里存档信息显示了玩家名称与时间戳,以及玩家存档时的游戏截图。
结语
本文介绍的只是Unity中实现文件存档的其中一点内容,文中的实现方式不一定适用于所有游戏,但其中介绍的思路与关键注意事项可以供大部分游戏参考。希望本文对大家有帮助,我们还会分享一些实际的游戏开发经验在Unity官方中文社区(unitychina.cn),请保持关注!
推荐阅读
活动 | 7月Unity技术路演 武汉西安双城开发者火热集结!
点击“阅读原文”进入Unity官方中文社区!