Unity3D中存档实现
最近我在进行一个RPG游戏项目开发时,遇到了游戏存档实现的问题,并对其进行了深入研究。我认为学习是一种自我探索的行为,所以不太喜欢按部就班的技术教程。现在,我将自己的思路和想法分享给大家,希望能给大家带来一些启发。
游戏存档概述
游戏存档是单机游戏中常见的机制,在网络游戏中无法体验到。每次玩完单机游戏后保存存档,那种感觉就像征战沙场的将军将陪伴自己一生金戈铁马的宝剑静静收入剑匣,再次打开时可能会不由自主地热泪盈眶。其实,我们每天的生活也像是一场游戏,有时候让我们怀念的或许不是游戏本身,而是那时的自己。游戏存档是我们在游戏世界里留下的痕迹,代表着我们曾经来过这个世界。
以RPG游戏为例,一个通用的游戏存档通常应包含以下内容:
- 角色信息:用于表征虚拟角色成长路线的信息,如生命值、魔法值、经验值等。
- 道具信息:涉及虚拟道具数量或作用的信息,像药品、道具、装备等。
- 场景信息:与游戏场景相关的信息,包括场景名称、角色在当前场景中的位置坐标等。
- 事件信息:和游戏事件相关的信息,例如主线任务、支线任务、触发性事件等。
从上述信息划分可以看出,游戏存档需要存储的信息较为复杂。接下来,我们将探讨Unity3D中的数据持久化方案——PlayerPrefs。
Unity3D的数据持久化方案PlayerPrefs
PlayerPrefs采用键值型的数据存储方案,支持int、string、float三种基本数据类型。通过键名可以获取对应的值,若值不存在则返回默认值。该方案本质上是将数据写入一个Xml文件。对于存储简单信息,PlayerPrefs是可行的,但对于存储游戏存档这种复杂的数据结构,它就显得力不从心了。
在数据持久化过程中,我们期望得到一个结构化的【游戏存档】实例,而松散的PlayerPrefs无法满足这一需求。因此,我们考虑采用游戏数据序列化的思路,常见的数据序列化方式主要有Xml和JSON两种。
使用Xml进行数据序列化时,通常有两种思路:手动建立数据实体和数据字符间的对应关系,以及基于XmlSerializer的数据序列化。其中,基于XmlSerializer的数据序列化利用了[Serializable]语法特性,帮助.NET完成数据实体和数据字符间的对应关系,这两种思路本质上是相同的。不过,Xml的优点是可读性强,缺点是冗余信息多。经过权衡,我决定采用JSON作为数据序列化方案。JSON在数据实体和数据字符间的对应关系上具有天然优势,它的主要功能就是将数据实体转化为字符串,并从字符串中解析出数据实体,整个过程较为流畅。下面我们来看具体的代码实现。
具体代码实现
1. JSON的序列化和反序列化
这里我们使用Newtonsoft.Json类库,它可以方便地实现序列化和反序列化。以下是具体代码:
/// <summary>
/// 将一个对象序列化为字符串
/// </summary>
/// <returns>序列化后的字符串</returns>
/// <param name="pObject">对象</param>
private static string SerializeObject(object pObject)
{
// 序列化后的字符串
string serializedString = string.Empty;
// 使用Json.Net进行序列化
serializedString = JsonConvert.SerializeObject(pObject);
return serializedString;
}
/// <summary>
/// 将一个字符串反序列化为对象
/// </summary>
/// <returns>反序列化后的对象</returns>
/// <param name="pString">字符串</param>
/// <param name="pType">对象类型</param>
private static object DeserializeObject(string pString, Type pType)
{
// 反序列化后的对象
object deserializedObject = null;
// 使用Json.Net进行反序列化
deserializedObject = JsonConvert.DeserializeObject(pString, pType);
return deserializedObject;
}
2. Rijandel加密/解密算法
考虑到存档数据的安全性,我们可以采用相关的加密/解密算法对序列化后的明文数据进行加密,以保证游戏存档数据的安全性。这里提供一个从MSDN上获取的Rijandel算法,大家若感兴趣可以自行深入研究。
/// <summary>
/// Rijndael加密算法
/// </summary>
/// <param name="pString">待加密的明文</param>
/// <param name="pKey">密钥,长度可以为:64位(byte[8])、128位(byte[16])、192位(byte[24])、256位(byte[32])</param>
/// <param name="iv">iv向量,长度为128(byte[16])</param>
/// <returns>加密后的密文</returns>
private static string RijndaelEncrypt(string pString, string pKey)
{
// 密钥
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
// 待加密明文数组
byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(pString);
// Rijndael解密算法
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor();
// 返回加密后的密文
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
}
/// <summary>
/// Rijndael解密算法
/// </summary>
/// <param name="pString">待解密的密文</param>
/// <param name="pKey">密钥,长度可以为:64位(byte[8])、128位(byte[16])、192位(byte[24])、256位(byte[32])</param>
/// <param name="iv">iv向量,长度为128(byte[16])</param>
/// <returns>解密后的明文</returns>
private static String RijndaelDecrypt(string pString, string pKey)
{
// 解密密钥
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
// 待解密密文数组
byte[] toEncryptArray = Convert.FromBase64String(pString);
// Rijndael解密算法
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
// 返回解密后的明文
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
3. 完整代码
以下是完整代码,我们提供了两个公开的方法GetData()和SetData()以及IO相关的辅助方法,实际使用时只需关注这些方法即可。
// Unity3D数据持久化辅助类
// 作者:秦元培
// 时间:2015年8月14日
using UnityEngine;
using System.Collections;
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using Newtonsoft.Json;
public static class IOHelper
{
/// <summary>
/// 判断文件是否存在
/// </summary>
public static bool IsFileExists(string fileName)
{
return File.Exists(fileName);
}
/// <summary>
/// 判断文件夹是否存在
/// </summary>
public static bool IsDirectoryExists(string fileName)
{
return Directory.Exists(fileName);
}
/// <summary>
/// 创建一个文本文件
/// </summary>
/// <param name="fileName">文件路径</param>
/// <param name="content">文件内容</param>
public static void CreateFile(string fileName, string content)
{
StreamWriter streamWriter = File.CreateText(fileName);
streamWriter.Write(content);
streamWriter.Close();
}
/// <summary>
/// 创建一个文件夹
/// </summary>
public static void CreateDirectory(string fileName)
{
// 文件夹存在则返回
if (IsDirectoryExists(fileName))
return;
Directory.CreateDirectory(fileName);
}
public static void SetData(string fileName, object pObject)
{
// 将对象序列化为字符串
string toSave = SerializeObject(pObject);
// 对字符串进行加密,32位加密密钥
toSave = RijndaelEncrypt(toSave, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
StreamWriter streamWriter = File.CreateText(fileName);
streamWriter.Write(toSave);
streamWriter.Close();
}
public static object GetData(string fileName, Type pType)
{
StreamReader streamReader = File.OpenText(fileName);
string data = streamReader.ReadToEnd();
// 对数据进行解密,32位解密密钥
data = RijndaelDecrypt(data, "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
streamReader.Close();
return DeserializeObject(data, pType);
}
/// <summary>
/// Rijndael加密算法
/// </summary>
/// <param name="pString">待加密的明文</param>
/// <param name="pKey">密钥,长度可以为:64位(byte[8])、128位(byte[16])、192位(byte[24])、256位(byte[32])</param>
/// <param name="iv">iv向量,长度为128(byte[16])</param>
/// <returns></returns>
private static string RijndaelEncrypt(string pString, string pKey)
{
// 密钥
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
// 待加密明文数组
byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(pString);
// Rijndael解密算法
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateEncryptor();
// 返回加密后的密文
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return Convert.ToBase64String(resultArray, 0, resultArray.Length);
}
/// <summary>
/// Rijndael解密算法
/// </summary>
/// <param name="pString">待解密的密文</param>
/// <param name="pKey">密钥,长度可以为:64位(byte[8])、128位(byte[16])、192位(byte[24])、256位(byte[32])</param>
/// <param name="iv">iv向量,长度为128(byte[16])</param>
/// <returns></returns>
private static String RijndaelDecrypt(string pString, string pKey)
{
// 解密密钥
byte[] keyArray = UTF8Encoding.UTF8.GetBytes(pKey);
// 待解密密文数组
byte[] toEncryptArray = Convert.FromBase64String(pString);
// Rijndael解密算法
RijndaelManaged rDel = new RijndaelManaged();
rDel.Key = keyArray;
rDel.Mode = CipherMode.ECB;
rDel.Padding = PaddingMode.PKCS7;
ICryptoTransform cTransform = rDel.CreateDecryptor();
// 返回解密后的明文
byte[] resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
return UTF8Encoding.UTF8.GetString(resultArray);
}
/// <summary>
/// 将一个对象序列化为字符串
/// </summary>
/// <returns>The object.</returns>
/// <param name="pObject">对象</param>
/// <param name="pType">对象类型</param>
private static string SerializeObject(object pObject)
{
// 序列化后的字符串
string serializedString = string.Empty;
// 使用Json.Net进行序列化
serializedString = JsonConvert.SerializeObject(pObject);
return serializedString;
}
/// <summary>
/// 将一个字符串反序列化为对象
/// </summary>
/// <returns>The object.</returns>
/// <param name="pString">字符串</param>
/// <param name="pType">对象类型</param>
private static object DeserializeObject(string pString, Type pType)
{
// 反序列化后的对象
object deserializedObject = null;
// 使用Json.Net进行反序列化
deserializedObject = JsonConvert.DeserializeObject(pString, pType);
return deserializedObject;
}
}
需要注意的是,我们的密钥是直接写在代码中的,这存在一定风险。一旦项目被反编译,密钥就会变得不安全。这里有两种解决方法:一是将密钥暴露给外部方法,在读取和写入数据时使用同一个密钥,密钥可以由机器MAC值生成,这样每台机器上的密钥都不同,可防止数据被破解;二是采用DLL混淆的方法,让反编译者无法看到代码内容,从而无法获得正确的密钥和存档内容。
4. 最终效果
最后,我们编写一个简单的测试脚本来验证效果:
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class TestSave : MonoBehaviour
{
/// <summary>
/// 定义一个测试类
/// </summary>
public class TestClass
{
public string Name = "张三";
public float Age = 23.0f;
public int Sex = 1;
public List<int> Ints = new List<int>()
{
1,
2,
3
};
}
void Start()
{
// 定义存档路径
string dirpath = Application.persistentDataPath + "/Save";
// 创建存档文件夹
IOHelper.CreateDirectory(dirpath);
// 定义存档文件路径
string filename = dirpath + "/GameData.sav";
TestClass t = new TestClass();
// 保存数据
IOHelper.SetData(filename, t);
// 读取数据
TestClass t1 = (TestClass)IOHelper.GetData(filename, typeof(TestClass));
Debug.Log(t1.Name);
Debug.Log(t1.Age);
Debug.Log(t1.Ints);
}
}
以上就是Unity3D中实现游戏存档的详细介绍,希望大家喜欢!