前言

不管开发什么游戏,游戏存档是个必不可少的功能,你可能需要保存玩家的一些信息,比如身上穿戴的装备,玩家角色所处的场景等各种信息,对于存档功能(数据持久化),Unity提供了原生技术Playerprefs,它的优点是理解和使用起来十分简单,缺点是对于大型数据存储时会力不从心,所以本文会介绍如何使用XML来实现游戏存档和存档加密的功能。

编程环境

  • Unity 5.2.2
  • OS X EI Capitan 10.11.6

你将学到什么?

  • 如何使用XML对数据进行序列化和反序列化操作。
  • 如何对数据进行加密和解密操作。
  • 不同平台下文件存放的具体路径和规则。

整体思路解析

数据存储和加密的主要逻辑思路:

  1. 使用XmlSerializer类对需要保存的数据类进行序列化操作,得到一串字符串。
  2. 将得到的字符串使用RijndaeManaged类和ICrytoTransform类进行加密操作,获得加密之后的字符串。
  3. 根据平台类型,确定文件保存的路径。
  4. 使用StreamWriter类将字符串保存到文件中。

数据加载和解密的主要逻辑思路:

  1. 根据存档文件的路径,使用StreamReader类读取文件中的内容(一串加密过的字符串)。
  2. 使用RijndaeManaged类和ICrytoTransform类对读取的文件内容进行解密操作,获得一串解密后的字符串。
  3. 使用XmlSerializer类对解密后的字符串进行反序列化操作,获得具体的游戏数据,并使用数据对游戏中的数据进行转换操作。

一、准备工作

在实现具体保存操作前,我们需要先实现我们要保存的游戏数据,本文使用一个简化的数据,假设我们保存的数据是MyPlayer类,里面记录着以下信息:

  • 玩家的名字
  • 玩家的等级
  • 玩家的武器(包括物品ID,物品名字信息)
  • 玩家的衣服(包括物品ID,物品名字信息)

下面我们就来具体实现这些数据,首先我们新建一个Unity工程,工程名字大家可以自定义,然后我们新建一个C#文件,命名为Item,打开item文件,并进行如下编辑:

Item代码

  • 在Item类中,我们定义了两个公共成员,_itemID和_name,分别代表物品的ID和名字。
  • 然后我们在构造函数里面初始化了物品ID和名称。
  • 最后我们又创建了一个构造函数,可以通过参数来指定Item的id和名字。

接下来我们再新建一个C#文件,命名为MyPlayer,打开Myplayer文件,并进行如下编辑:

MyPlayer代码

  • 在MyPlayer类中,我们定义了4个公共成员,他们是_id,_name,_weapon和_clothes,他们分别代表玩家的ID,名字,所拥有的武器和衣服。
  • 然后我们实现了MyPlayer的构造函数,在构造函数里面我们实例化了他的4个公共成员。

二、实现XmlManager的序列化与写入操作

有了需要存储的数据,下面我们可以来实现如何将数据用XML来序列化,并且将序列化的数据写入到文件中。

首先我们新建一个C#脚本,命名为XmlManager,然后打开脚本进行如下编辑:

XmlManager关于序列化和写入的代码

  • 通过上图我们可以看到,在XmlManager脚本中,我们实现了3个方法
  • 在serializeObject方法中我们首先创建MemoryStream对象,这是为了后面我们创建XmlTextWriter类时的准备,因为我们会通过指定流和编码方式来创建XmlTextWriter的实例对象。
  • 接着我们通过指定数据类型创建了一个Xmlserializer的实例对象xs,然后通过调用xs的Serialize方法对传入的pObject进行序列化。
  • 然后我们把xmlTextWriter.BaseStream强制转换成MemoryStream类型,并赋值给mStream。
  • 最后通过调用UTF8ByteArrayToString方法将mStream数据转换成string类型,并返回数据。
  • 在CreateXMl方法中,我们通过传入的参数,指定了文件保存的位置,以及需要保存的具体数据,然后通过StreamWriter类将数据写入到文件中。
  • 在UTF8ByteArrayToString方法中,我们通过UTF8Encoding将byte[]类型数据转换成了String类型。

三、实现GameDataManager保存操作

首先在场景中新增一个空的对象,然后将其命名为DataController,然后在上面挂载一个我们新建的C#脚本GameDataManager

DataController对象与其挂载的脚本

然后我们打开GameDataManager脚本进行如下的编辑:

GameDataManager的保存和获取路径代码

编写上图中的代码后,我们回到Unity编辑器,然后运行,之后我们便会发现Project视窗中多了一个名为ZuiData的文件,如下图:

**Project**视窗中**ZuiData**文件

然后我们打开ZuiData文件,就会发现里面保存着_myPlayer对象的数据,如下图:

ZuiData的文件内容

到此,我们学会了将游戏数据序列化并写入到文件中的操作了。

四、实现XmlManager的反序列化和读取操作

学会了保存数据后,下一步我们就要来实现读取文件数据,并且将其反序列化,成为我们可以使用的对象。

我们再次打开XmlManager脚本,新增以下代码:

XmlManager中的反序列化和读取代码

  • 首先我们看deserializeObject方法,我们通过传入的参数ty,确定XmlSerializer需要反序列化的类型,然后需要反序列化的内容从string转换成byte[]类型,最后调用xs.Deserialize方法进行反序列化操作,并返回其结果。
  • 接着我们看loadXML方法,有一个参数,是需要读取的文件名称,然后创建一个StreamReader类型的对象,然后调用其方法ReadToEnd进行读取操作,最后返回读取的内容
  • stringToUTF8ByteArrayhasFile就很好理解了,一个是将string类型装换成byte[]类型,一个是通过文件名判断该文件是否存在。

五、实现GameDataManager的读取数据操作

实现了XmlManager的反序列化和读取操作后,我们就可以在GameDataManager中实现将xml的数据读取,并且把这些数据转换成我们需要使用的类型,比如转换成我们_myPlayer的信息。

接下来,我们打开GameDataManager脚本,并新增以下代码:

GameDataManager的读取和打印代码

  • 首先在load方法中,先获取文件存储的路径,然后判断文件是否存在,如果不存在,则在后台打印提示信息,接下来,调用xm.loadXML方法读取文件中的数据,读取出来的数据是一段字符串,然后我们在调用xm.deserializeObject方法把数据转换成MyPlayer类型的数据,最后如果数据不为空,我们就把这些数据赋值给_myPlayer对象。
  • pressLoadButton方法是后面我们制作读取按钮时会用的方法,里面主要做了两件事情,一是调用load方法,读取数据,二是调用printData方法打印_myPlayer的部分属性。
  • printData方法中,我们调用Debug.log方法打印出我们想要看的_myPlayer的属性,而这里我们打印的属性,是后面我们修改过具体内容的几个数据,打印出来就是为了查看是否修改成功。

制作读取按钮

有了上面这些方法后,为了在实际演示中,让我们可以看到数据的读取后的改变,我们在项目中新建一个按钮,来触发数据读取的操作。

读取数据按钮

我们在项目中新建一个名为“LoadButton”的按钮,然后将其内容改为“读取数据”,然后我们在按钮的点击逻辑上挂载GameDataManager中的pressLoadButton方法,如下图:

挂载pressLoadButton方法到LoadButton上

实现按钮后,我们打开ZuiData文件,将玩家的ID改为99,玩家名字改为“ZuiPlayer”,武器的名称改为“Eagles”,如下图:

修改ZuiData文件内容

修改之后,我们再次打开GameDataManager脚本,把Start方法中的内容全部注释掉,如下图:

注释Start方法中的代码

最后我们回到Unity编辑器,然后运行程序,点击读取数据按钮,然后查看后台打印出来的数据是否与我们修改过后的数据一致,如无意外,效果如下图:

最终效果图

到此,我们学会了XML数据的读取和反序列化。

六、对文件数据进行加密和解密

虽然我们现在学会了使用xml进行数据的存储和读取,但是就想我们上面读取操作时一样,我们可以直接通过改写ZuiData文件里面的内容,从而改变游戏的数据,这样对于游戏数据来说是很不安全的,所以我们最好对游戏最终保存的数据进行一些加密操作,这样就无法通过文件直接修改游戏的数据了。

我们再次打开XmlManager脚本,然后新增以下代码:

XmlManager中加密和解密的代码

  • 首先我们在前面引入了新的头文件System.Security.Cryptography,我们下面需要用的RijndaelManagedICryptTransform类,都是属于其中。
  • 接下来我们定义了我们加密和解密所需要用的密钥,具体的数字可以自定义,但是必须一共是32位。
  • 然后我们先跳到上图最后的getRijndaelManaged方法,这里面我们主要是创建并定义我们加密和解密的方式,我们定义了一个RijndaelManaged对象,然后设置密钥为_keyArray(也就是我们之前定义的密钥),然后设置对称解密算法的运算模式和填充模式(关于运算模式和填充模式的详细说明,大家可以参见最后面的参考链接),最后我们返回RijndaelManaged的实例化对象。
  • 接下来我们看到encrypt方法,这里面我们主要是把传入的数据,进行加密操作,然后返回加密后的数据,首先我们定义了一个ICryptTransform类型的对象,并且他是加密模式的,然后我们把需要加密的数据类型转换成byte[]类型,调用TransformFinalBlock方法得到加密后的数据,最后将这个数据转换成string类型并返回。
  • 最后我们来看decrypt方法,这里我们主要把传入的数据,进行解密操作,然后返回解密后的数据,首先我们定义了一个ICryptTransform类型的对象,并且他是解密模式的,然后我们把需要解密的数据转换成byte[]类型,调用TransformFinalBlock方法获得解密后的数据,最后将这个数据转换成string类型并返回。

完成了这些主要的方法后,我们还需要对XmlManager脚本进行一些小修改,具体见下图:

XmlManager中的小修改

做完这些改动后,我们再把GameDataManager脚本中的Start方法中的代码注释取消掉,让其恢复作用,最后我们回到Unity编辑器,运行程序,可以看到我们save和load操作都是正常运行的,而这个时候我们再次打开ZuiData文件,就会发现文件中的内容变成了一些乱码,这样就无法通过修改存档来改变游戏的数据了。

加密后的ZuiData文件内容

参考阅读

MSDN 关于RijndaelManaged类的说明
MSDN 关于ICryptoTransform类的说明

补充内容

  • 需要序列化的成员必须是public的,私有的是不会被序列化的,如果某个成员变量不想序列化,有两种方法,一是设置为私有,二是使用[XmlIgnore]修饰,如下图:

_clothes就不会被序列化

  • 如果一个成员变量的值为null,序列化时不会记录任何信息。
  • 自定义序列化类,必须要有默认的构造函数(即不带任何参数的构造函数),否则会报错。



作者:Zui
链接:https://www.jianshu.com/p/29e3b50deac1
來源:简书
 

10-03 11:02