Unity3D游戏开发之游戏读/存档功能在Unity3D中的实现

喜欢我的博客请记住我的名字:秦元培,我的博客地址是: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);
    }


}

  脚本运行结果:

p1

  加密后游戏存档:

p2

  好了,这就是今天的内容了,希望大家能够喜欢,有什么问题能够给我留言,谢谢!


  感谢风宇冲Unity3D教程宝典之两步实现超有用的XML存档一文提供相关思路!

喜欢我的博客请记住我的名字:秦元培,我的博客地址是:http://qinyuanpei.com
转载请注明出处,本文作者:秦元培。 本文出处:http://blog.csdn.net/qinyuanpei/article/details/39717795

原文地址:https://www.cnblogs.com/liguangsunls/p/7280414.html