喜欢我的博客请记住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com

转载请注明出处,本文作者:秦元培, 本文出处:http://blog.csdn.net/qinyuanpei/article/details/47775979

  大家好,我是秦元培,欢迎大家关注我的博客。近期博客的更新频率基本直降到冰点,由于这段时间实在是忙得没有时间来写博客了。今天想和大家分享的内容是RPG游戏中游戏存档的实现。由于近期在做一个RPG游戏的项目,所以遇到这个问题就随时记录下来,在对知识进行总结的同一时候能够将这样的思路或者想法分享给大家,这是一件快乐而幸运的事情。

我讨厌写按部就班的技术教程,由于我认为学习是一种自我的探索行为,假设一切都告诉你了,探索的过程便会变得没有意义了。

  游戏存档是一种在单机游戏中特别常见的机制。这样的机制是你在玩网络游戏的时候无法体验到的。你知道每次玩完一款单机游戏都会把游戏存档保存起来是一种如何的感觉吗?它就像是一个征战沙场的将军将陪伴自己一生金戈铁马的宝剑静静地收入剑匣。然而每一次打开它的时候都会情不自禁的热泪盈眶。人的本性事实上就是游戏,我们每一天发生的故事何尝不是一个游戏?有时候让我们怀念的可能并非游戏本身。而仅仅是搁浅在时光里的那时的我们。好了,游戏存档是我们在游戏世界里雪泥鸿爪,它代表了我们以前来到过这个世界。以RPG游戏为例。一个一般化的游戏存档应该囊括以下内容:

  • 角色信息:指一切表征虚拟角色成长路线的信息,如生命值、魔法值、经验值等等。
  • 道具信息:指一切表征虚拟道具数量或者作用的信息,如药品、道具、装备等等。
  • 场景信息:指一切和游戏场景相关的信息。如场景名称、角色在当前场景中的位置坐标等等。
  • 事件信息:指一切和游戏事件相关的信息,如主线任务、支线任务、触发性事件等等。

  从以上信息划分的层次来看,我们能够发如今游戏存档中要储存的信息相对是比較复杂的。那么我们这里不得不说说Unity3D中的数据持久化方案PlayerPrefs。该方案採用的是一种键值型的数据存储方案。支持int、string、float三种基本数据类型,通过键名来获取相相应的数值,当值不存在时将返回一个默认值。这样的数据存储方案本质上是将数据写入到一个Xml文件。

这样的方案假设用来存储简单的信息是没有问题的,但是假设用它来存储游戏存档这样负责的数据结构就显得力不从心了。一个更为重要的问题是在数据持久化的过程中我们希望得到是一个结构化的【游戏存档】实例,显然此时松散的PlayerPrefs是不能满足我们的要求的。因此我们想到了将游戏数据序列化的思路,常见的数据序列化思路主要有Xml和JSON两种形式。在使用Xml的数据序列化方案的时候通常有两种思路,即手动建立数据实体和数据字符间的相应关系基于XmlSerializer的数据序列化。当中基于XmlSerializer的数据序列化是利用了[Serializable]这样的语法特性来帮助.NET完毕数据实体和数据字符间的相应关系,两种思路本质上一样的。

但是我们知道Xml的长处是可读性强,缺点是冗余信息多,因此在权衡了两种方案的利弊后。我决定採用JSON来作为数据序列化的方案,并且JSON在数据实体和数据字符间的相应关系上有着天然的优势,JSON所做的事情不就是将数据实体转化为字符串和从一个字符串中解析出数据实体吗?所以整个方案基本一气呵成。好了,以下我们来看详细的代码实现过程吧!

一、JSON的序列化和反序列化

  这里我使用的是Newtonsoft.Json这个类库,相信大家都是知道的了。因此。序列化和反序列化特别简单。

/// <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;
}

二、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>
/// ijndael解密算法
/// </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);
}

三、完整代码

  好了,以下给出完整代码。我们这里提供了两个公开的方法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>
/// ijndael解密算法
/// </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混淆的方法让反编译者无法看到代码中的内容。这样就无法获得正确的密钥从而无法获得存档里的内容了。

四、终于效果

好了,最后我们来写一个简单的測试脚本:

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教程宝典之两步实现超有用的XML存档一文提供相关思路!

喜欢我的博客请记住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com

转载请注明出处,本文作者:秦元培。 本文出处:http://blog.csdn.net/qinyuanpei/article/details/39717795

Unity3D游戏开发之游戏读/存档功能在Unity3D中的实现的更多相关文章

  1. Unity 2D游戏开发教程之摄像头追踪功能

    Unity 2D游戏开发教程之摄像头追踪功能 上一章,我们创建了一个简单的2D游戏.此游戏中的精灵有3个状态:idle.left和right.这看起来确实很酷!但是仅有的3个状态却限制了精灵的能力,以 ...

  2. [libGDX游戏开发教程]使用libGDX进行游戏开发(1)-游戏设计

    声明:<使用Libgdx进行游戏开发>是一个系列,文章的原文是<Learning Libgdx Game Development>,大家请周知.后续的文章连接在这里 使用Lib ...

  3. 《Cocos2d-x游戏开发实战精解》学习笔记3--在Cocos2d-x中播放声音

    <Cocos2d-x游戏开发实战精解>学习笔记1--在Cocos2d中显示图像 <Cocos2d-x游戏开发实战精解>学习笔记2--在Cocos2d-x中显示一行文字 之前的内 ...

  4. 游戏开发设计模式之对象池模式(unity3d 示例实现)

    前篇:游戏开发设计模式之命令模式(unity3d 示例实现) 博主才学尚浅,难免会有错误,尤其是设计模式这种极富禅意且需要大量经验的东西,如果哪里书写错误或有遗漏,还请各位前辈指正. 原理:从一个固定 ...

  5. JavaFX横幅类游戏开发 教训 游戏贴图

    上一节课,我们即将完成战旗Demo有了一个大概的了解.教训这,我们将学习绘制游戏地图. 由于JavaFX 2.2中添加了Canvas相关的功能,我们就能够使用Canvas来实现游戏绘制了. 游戏地图绘 ...

  6. 《Cocos2d-x游戏开发实战精解》学习笔记1--在Cocos2d中显示图像

    Cocos2d-x中的图像是通过精灵类来显示的.在Cocos2d-x中游戏中的每一个角色.怪物.道具都可以理解成是一个精灵,游戏背景作为一种特殊的单位将其理解成是一个精灵也没有什么不妥.在源文件本章目 ...

  7. java游戏开发杂谈 - 游戏物体

    现实生活中,有很多物体,每个物体的长相.行为都不同. 物体存在于不同的空间内,它只在这个空间内发生作用. 物体没用了,空间就把它剔除,不然既占地方,又需要花精力管理. 需要它的时候,就把它造出来,不需 ...

  8. java游戏开发杂谈 - 游戏编程浅析

    每个游戏,你所看到的它的一切,都是计算机画出来的! 地图是画出来,人物是画出来的,树木建筑是画出来的,菜单按钮是画出来的,滚动的文字.闪烁的图标.云雾烟火,都是画出来的. 游戏编程,所要做的,就是控制 ...

  9. 【Unity游戏开发】用C#和Lua实现Unity中的事件分发机制EventDispatcher

    一.简介 最近马三换了一家大公司工作,公司制度规范了一些,因此平时的业余时间多了不少.但是人却懒了下来,最近这一个月都没怎么研究新技术,博客写得也是拖拖拉拉,周六周天就躺尸在家看帖子.看小说,要么就是 ...

随机推荐

  1. iptables禁止外网访问redis server服务默认端口6379的命令

    //只允许127.0.0.1访问6379 iptables -A INPUT -s 127.0.0.1 -p tcp --dport 6379 -j ACCEPT //其他ip访问全部拒绝 iptab ...

  2. spring集成redis,集成redis集群

    原文:http://chentian114.iteye.com/blog/2292323 1.通过spring-data-redis集成redis pom.xml依赖包 <project xml ...

  3. CSS3:图片水平垂直居中

    加上这两个就行 display:-webkit-box;     显示成盒子模式 -webkit-box-align:center;   垂直居中 -webkit-box-pack:center;   ...

  4. sqlserver 删除临时表

    sqlserver 删除临时表 if object_id('tempdb..#tempTable') is not null Begin drop table #tempTable End

  5. Swift,函数

    1.无参数无输出的函数 func a(){ print("HI") } a() //HI 2.有参数有输出的函数 func add(a:Int,b:Int)->Int{ // ...

  6. easyUI中datagrid控制获取指定行数的数据

    直接上代码: var rows=$('#detail').datagrid('getRows');//获取所有当前加载的数据行 var row=rows[0];// 行数从 0 开始 项目中代码: v ...

  7. ISP图像调试工程师——3D和2D降噪(熟悉图像预处理和后处理技术)

    2D降噪:只在2维空间域上进行降噪处理.基本方法:对一个像素将其与周围像素平均,平均后噪声降低,但缺点是会造成画面模糊,特别是物体边缘部分.因此对这种算法的改进主要是进行边缘检测,边缘部分的像素不用来 ...

  8. EL表达式介绍(2)

    1. EL关系运算符: 关系运算符 说明 范例 结果 == 或 eq 等于 ${5==5}或${5eq5} true != 或 ne 不等于 ${5!=5}或${5ne5} false < 或 ...

  9. grpc(3):使用 golang 开发 grpc 服务端和client

    1,关于grpc-go golang 能够能够做grpc的服务端和client. 官网的文档: http://www.grpc.io/docs/quickstart/go.html https://g ...

  10. IntelliJ IDEA 控制台中文乱码

    1. 预热 刚刚接触IntelliJ IDEA几天,在易用性方面的确比Eclipse好很多,比较智能,各种插件.工具都已经集成,和Mac OS X类似——开箱即用. 但是还是老大难问题——中文乱码,让 ...