喜欢我的博客请记住我的名字:秦元培,我的博客地址是: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. 利用Java位运算符,完成Unsigned转换(无符号)

    方案二:利用Java位运算符,完成Unsigned转换. 正常情况下,Java提供的数据类型是有符号signed类型的,可以通过位运算的方式得到它们相对应的无符号值,参见几个方法中的代码: publi ...

  2. Linux下CURL设置请求超时时间

    使用CURL时,有两个超时时间:一个是连接超时时间,另一个是数据传输的最大允许时间. 连接超时时间用--connect-timeout参数来指定,数据传输的最大允许时间用-m参数来指定. 例如: cu ...

  3. android出现段错误时的查找定位的方法

    android出现段错误时的查找方法,例如出现log: - ::): Fatal signal (SIGSEGV) at ), thread (SurfaceFlinger) - ::): *** * ...

  4. centos7修改yum下载源为阿里源

    在国内很多yum源不好用,所以改成国内的源很有必要 首先,切换到yum源目录 cd /etc/yum.repos.d 备份一下 sudo mv CentOS-Base.repo CentOS-Base ...

  5. VR/AR工作原理、目前存在的技术问题

    http://blog.csdn.net/liulong1567/article/details/50686558 摘要: 这些挑战,每一个都还需要很多努力才能解决.目前它们很多都还不到量变(只是需要 ...

  6. Hadoop之Hbase详解

    1.什么是Hbase HBASE是一个高可靠性.高性能.面向列.可伸缩的分布式存储系统, hbase是列式的分布式数据库 1.2.HBASE优势: 1)线性扩展,随着数据量增多可以通过节点扩展进行支撑 ...

  7. Hadoop之Azkaban详解

    工作流调度器azkaban1 为什么需要工作流调度系统 1)一个完整的数据分析系统通常都是由大量任务单元组成:shell脚本程序,java程序,mapreduce程序.hive脚本等 2)各任务单元之 ...

  8. 基于Spark机器学习和实时流计算的智能推荐系统

    概要: 随着电子商务的高速发展和普及应用,个性化推荐的推荐系统已成为一个重要研究领域. 个性化推荐算法是推荐系统中最核心的技术,在很大程度上决定了电子商务推荐系统性能的优劣,决定着是否能够推荐用户真正 ...

  9. 转: 私人珍藏的Chrome插件,吐血推荐

    转:来自 http://stormzhang.com/devtools/2016/01/15/google-chrome-extension/

  10. ITIL是什么

    ITIL是什么 ITIL&CMDB 番外: ITIL&互联网 ITIL是什么 ITIL即IT基础架构库(Information Technology Infrastructure Li ...