Unity3D游戲讀/存檔功能的實現(xiàn)

原文鏈接:http://blog.csdn.net/qinyuanpei/article/details/47775979作者:秦元培

游戲存檔是一種在單機(jī)游戲中特別常見的機(jī)制优床,這種機(jī)制是你在玩網(wǎng)絡(luò)游戲的時候無法體驗到的在辆,你知道每次玩完一款單機(jī)游戲都會把游戲存檔保存起來是一種怎樣的感覺嗎?它就像是一個征戰(zhàn)沙場的將軍將陪伴自己一生金戈鐵馬的寶劍靜靜地收入劍匣茎芋,然而每一次打開它的時候都會不由自主的熱淚盈眶。人的本性其實就是游戲压语,我們每一天發(fā)生的故事何嘗不是一個游戲岭辣?有時候讓我們懷念的可能并不是游戲本身,而只是擱淺在時光里的那時的我們引颈。好了,游戲存檔是我們在游戲世界里雪泥鴻爪境蜕,它代表了我們曾經(jīng)來到過這個世界蝙场。以RPG游戲為例,一個一般化的游戲存檔應(yīng)該囊括以下內(nèi)容:

角色信息:指一切表征虛擬角色成長路線的信息粱年,如生命值售滤、魔法值、經(jīng)驗值等等台诗。

道具信息:指一切表征虛擬道具數(shù)量或者作用的信息完箩,如藥品、道具拉队、裝備等等弊知。

場景信息:指一切和游戲場景相關(guān)的信息,如場景名稱氏仗、角色在當(dāng)前場景中的位置坐標(biāo)等等吉捶。

事件信息:指一切和游戲事件相關(guān)的信息夺鲜,如主線任務(wù)皆尔、支線任務(wù)、觸發(fā)性事件等等币励。

從以上信息劃分的層次來看慷蠕,我們可以發(fā)現(xiàn)在游戲存檔中要儲存的信息相對是比較復(fù)雜的,那么我們這里不得不說說Unity3D中的數(shù)據(jù)持久化方案PlayerPrefs食呻。該方案采用的是一種鍵值型的數(shù)據(jù)存儲方案流炕,支持int、string仅胞、float三種基本數(shù)據(jù)類型每辟,通過鍵名來獲取相對應(yīng)的數(shù)值,當(dāng)值不存在時將返回一個默認(rèn)值干旧。這種數(shù)據(jù)存儲方案本質(zhì)上是將數(shù)據(jù)寫入到一個Xml文件渠欺。這種方案如果用來存儲簡單的信息是沒有問題的,可是如果用它來存儲游戲存檔這樣負(fù)責(zé)的數(shù)據(jù)結(jié)構(gòu)就顯得力不從心了椎眯。一個更為重要的問題是在數(shù)據(jù)持久化的過程中我們希望得到是一個結(jié)構(gòu)化的【游戲存檔】實例挠将,顯然此時松散的PlayerPrefs是不能滿足我們的要求的胳岂。因此我們想到了將游戲數(shù)據(jù)序列化的思路,常見的數(shù)據(jù)序列化思路主要有Xml和JSON兩種形式舔稀,在使用Xml的數(shù)據(jù)序列化方案的時候通常有兩種思路乳丰,即手動建立數(shù)據(jù)實體和數(shù)據(jù)字符間的對應(yīng)關(guān)系和基于XmlSerializer的數(shù)據(jù)序列化。其中基于XmlSerializer的數(shù)據(jù)序列化是利用了[Serializable]這樣的語法特性來幫助.NET完成數(shù)據(jù)實體和數(shù)據(jù)字符間的對應(yīng)關(guān)系内贮,兩種思路本質(zhì)上一樣的产园。可是我們知道Xml的優(yōu)點(diǎn)是可讀性強(qiáng)贺归,缺點(diǎn)是冗余信息多淆两,因此在權(quán)衡了兩種方案的利弊后,我決定采用JSON來作為數(shù)據(jù)序列化的方案拂酣,而且JSON在數(shù)據(jù)實體和數(shù)據(jù)字符間的對應(yīng)關(guān)系上有著天然的優(yōu)勢秋冰,JSON所做的事情不就是將數(shù)據(jù)實體轉(zhuǎn)化為字符串和從一個字符串中解析出數(shù)據(jù)實體嗎?所以整個方案基本一氣呵成婶熬。好了剑勾,下面我們來看具體的代碼實現(xiàn)過程吧!

一赵颅、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進(jìn)行序列化
        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進(jìn)行反序列化
        deserializedObject=JsonConvert.DeserializeObject(pString,pType);
        return deserializedObject;
    }

二、Rijandel加密/解密算法
??因為我們這里要做的是一個游戲存檔的方案設(shè)計募寨,因為考慮到存檔數(shù)據(jù)的安全性族展,我們可以考慮采用相關(guān)的加密/解密算法來實現(xiàn)對序列化后的明文數(shù)據(jù)進(jìn)行加密,這樣可以從一定程度上保證游戲存檔數(shù)據(jù)的安全性拔鹰。因為博主并沒有深入地研究過加密/解密方面的內(nèi)容仪缸,所以這里僅僅提供一個從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);
        //待加密明文數(shù)組
        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);
        //待解密密文數(shù)組
        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相關(guān)的輔助方法拴还,我們在實際使用的時候只需要關(guān)注這些方法就可以了!

/**
 * Unity3D數(shù)據(jù)持久化輔助類
 * 作者:秦元培
 * 時間: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>
    /// 創(chuàng)建一個文本文件    
    /// </summary>
    /// <param name="fileName">文件路徑</param>
    /// <param name="content">文件內(nèi)容</param>
    public static void CreateFile(string fileName,string content)
    {
        StreamWriter streamWriter = File.CreateText(fileName);
        streamWriter.Write(content);
        streamWriter.Close();
    }

    /// <summary>
    /// 創(chuàng)建一個文件夾
    /// </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);
        //對字符串進(jìn)行加密,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();
        //對數(shù)據(jù)進(jìn)行解密欧聘,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);
        //待加密明文數(shù)組
        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);
        //待解密密文數(shù)組
        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進(jìn)行序列化
        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進(jìn)行反序列化
        deserializedObject=JsonConvert.DeserializeObject(pString,pType);
        return deserializedObject;
    }
}

這里我們的密鑰是直接寫在代碼中的片林,這樣做其實是有風(fēng)險的,因為一旦我們的項目被反編譯,我們這里的密鑰就變得很不安全了拇厢。這里有兩種方法爱谁,一種是把密鑰暴露給外部方法,即在讀取數(shù)據(jù)和寫入數(shù)據(jù)的時候使用同一個密鑰即可孝偎,而密鑰可以采取由機(jī)器MAC值生成的方法访敌,這樣每臺機(jī)器上的密鑰都是不同的可以防止數(shù)據(jù)被破解;其次可以采用DLL混淆的方法讓反編譯者無法看到代碼中的內(nèi)容衣盾,這樣就無法獲得正確的密鑰從而無法獲得存檔里的內(nèi)容了寺旺。

四、最終效果

好了势决,最后我們來寫一個簡單的測試腳本:

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";
        //創(chuàng)建存檔文件夾
        IOHelper.CreateDirectory (dirpath);
        //定義存檔文件路徑
        string filename = dirpath + "/GameData.sav";
        TestClass t = new TestClass ();
        //保存數(shù)據(jù)
        IOHelper.SetData (filename,t);
        //讀取數(shù)據(jù)
        TestClass t1 = (TestClass)IOHelper.GetData(filename,typeof(TestClass));

        Debug.Log(t1.Name);
        Debug.Log(t1.Age);
        Debug.Log(t1.Ints);
    }
}

腳本執(zhí)行結(jié)果:

p1

加密后游戲存檔:

p2

原文鏈接:http://blog.csdn.net/qinyuanpei/article/details/47775979作者:秦元培

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末阻塑,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子果复,更是在濱河造成了極大的恐慌陈莽,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虽抄,死亡現(xiàn)場離奇詭異走搁,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)迈窟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門私植,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人车酣,你說我怎么就攤上這事曲稼。” “怎么了湖员?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵贫悄,是天一觀的道長。 經(jīng)常有香客問我破衔,道長清女,這世上最難降的妖魔是什么钱烟? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任晰筛,我火速辦了婚禮,結(jié)果婚禮上拴袭,老公的妹妹穿的比我還像新娘读第。我一直安慰自己,他們只是感情好拥刻,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布怜瞒。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吴汪。 梳的紋絲不亂的頭發(fā)上惠窄,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音漾橙,去河邊找鬼杆融。 笑死,一個胖子當(dāng)著我的面吹牛霜运,可吹牛的內(nèi)容都是我干的脾歇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼淘捡,長吁一口氣:“原來是場噩夢啊……” “哼藕各!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起焦除,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤激况,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后膘魄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體誉碴,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年瓣距,在試婚紗的時候發(fā)現(xiàn)自己被綠了黔帕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡蹈丸,死狀恐怖成黄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逻杖,我是刑警寧澤奋岁,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站荸百,受9級特大地震影響闻伶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜够话,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一蓝翰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧女嘲,春花似錦畜份、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春钙态,著一層夾襖步出監(jiān)牢的瞬間慧起,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工册倒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留完慧,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓剩失,卻偏偏與公主長得像屈尼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拴孤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容