原文鏈接: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é)果:
加密后游戲存檔:
原文鏈接:http://blog.csdn.net/qinyuanpei/article/details/47775979作者:秦元培