前言
不管開發(fā)什么游戲,游戲存檔是個(gè)必不可少的功能,你可能需要保存玩家的一些信息,比如身上穿戴的裝備,玩家角色所處的場(chǎng)景等各種信息,對(duì)于存檔功能(數(shù)據(jù)持久化),Unity提供了原生技術(shù)Playerprefs,它的優(yōu)點(diǎn)是理解和使用起來十分簡(jiǎn)單,缺點(diǎn)是對(duì)于大型數(shù)據(jù)存儲(chǔ)時(shí)會(huì)力不從心,所以本文會(huì)介紹如何使用XML來實(shí)現(xiàn)游戲存檔和存檔加密的功能。
編程環(huán)境
- Unity 5.2.2
- OS X EI Capitan 10.11.6
你將學(xué)到什么?
- 如何使用XML對(duì)數(shù)據(jù)進(jìn)行序列化和反序列化操作。
- 如何對(duì)數(shù)據(jù)進(jìn)行加密和解密操作。
- 不同平臺(tái)下文件存放的具體路徑和規(guī)則。
整體思路解析
數(shù)據(jù)存儲(chǔ)和加密的主要邏輯思路:
- 使用XmlSerializer類對(duì)需要保存的數(shù)據(jù)類進(jìn)行序列化操作,得到一串字符串。
- 將得到的字符串使用RijndaeManaged類和ICrytoTransform類進(jìn)行加密操作,獲得加密之后的字符串。
- 根據(jù)平臺(tái)類型,確定文件保存的路徑。
- 使用StreamWriter類將字符串保存到文件中。
數(shù)據(jù)加載和解密的主要邏輯思路:
- 根據(jù)存檔文件的路徑,使用StreamReader類讀取文件中的內(nèi)容(一串加密過的字符串)。
- 使用RijndaeManaged類和ICrytoTransform類對(duì)讀取的文件內(nèi)容進(jìn)行解密操作,獲得一串解密后的字符串。
- 使用XmlSerializer類對(duì)解密后的字符串進(jìn)行反序列化操作,獲得具體的游戲數(shù)據(jù),并使用數(shù)據(jù)對(duì)游戲中的數(shù)據(jù)進(jìn)行轉(zhuǎn)換操作。
一、準(zhǔn)備工作
在實(shí)現(xiàn)具體保存操作前,我們需要先實(shí)現(xiàn)我們要保存的游戲數(shù)據(jù),本文使用一個(gè)簡(jiǎn)化的數(shù)據(jù),假設(shè)我們保存的數(shù)據(jù)是MyPlayer類,里面記錄著以下信息:
- 玩家的名字
- 玩家的等級(jí)
- 玩家的武器(包括物品ID,物品名字信息)
- 玩家的衣服(包括物品ID,物品名字信息)
下面我們就來具體實(shí)現(xiàn)這些數(shù)據(jù),首先我們新建一個(gè)Unity工程,工程名字大家可以自定義,然后我們新建一個(gè)C#文件,命名為Item,打開item文件,并進(jìn)行如下編輯:

- 在Item類中,我們定義了兩個(gè)公共成員,_itemID和_name,分別代表物品的ID和名字。
- 然后我們?cè)跇?gòu)造函數(shù)里面初始化了物品ID和名稱。
- 最后我們又創(chuàng)建了一個(gè)構(gòu)造函數(shù),可以通過參數(shù)來指定Item的id和名字。
接下來我們?cè)傩陆ㄒ粋€(gè)C#文件,命名為MyPlayer,打開Myplayer文件,并進(jìn)行如下編輯:

- 在MyPlayer類中,我們定義了4個(gè)公共成員,他們是_id,_name,_weapon和_clothes,他們分別代表玩家的ID,名字,所擁有的武器和衣服。
- 然后我們實(shí)現(xiàn)了MyPlayer的構(gòu)造函數(shù),在構(gòu)造函數(shù)里面我們實(shí)例化了他的4個(gè)公共成員。
二、實(shí)現(xiàn)XmlManager的序列化與寫入操作
有了需要存儲(chǔ)的數(shù)據(jù),下面我們可以來實(shí)現(xiàn)如何將數(shù)據(jù)用XML來序列化,并且將序列化的數(shù)據(jù)寫入到文件中。
首先我們新建一個(gè)C#腳本,命名為XmlManager,然后打開腳本進(jìn)行如下編輯:

- 通過上圖我們可以看到,在XmlManager腳本中,我們實(shí)現(xiàn)了3個(gè)方法
- 在serializeObject方法中我們首先創(chuàng)建MemoryStream對(duì)象,這是為了后面我們創(chuàng)建XmlTextWriter類時(shí)的準(zhǔn)備,因?yàn)槲覀儠?huì)通過指定流和編碼方式來創(chuàng)建XmlTextWriter的實(shí)例對(duì)象。
- 接著我們通過指定數(shù)據(jù)類型創(chuàng)建了一個(gè)Xmlserializer的實(shí)例對(duì)象xs,然后通過調(diào)用xs的Serialize方法對(duì)傳入的pObject進(jìn)行序列化。
- 然后我們把xmlTextWriter.BaseStream強(qiáng)制轉(zhuǎn)換成MemoryStream類型,并賦值給mStream。
- 最后通過調(diào)用UTF8ByteArrayToString方法將mStream數(shù)據(jù)轉(zhuǎn)換成string類型,并返回?cái)?shù)據(jù)。
- 在CreateXMl方法中,我們通過傳入的參數(shù),指定了文件保存的位置,以及需要保存的具體數(shù)據(jù),然后通過StreamWriter類將數(shù)據(jù)寫入到文件中。
- 在UTF8ByteArrayToString方法中,我們通過UTF8Encoding將byte[]類型數(shù)據(jù)轉(zhuǎn)換成了String類型。
三、實(shí)現(xiàn)GameDataManager保存操作
首先在場(chǎng)景中新增一個(gè)空的對(duì)象,然后將其命名為DataController,然后在上面掛載一個(gè)我們新建的C#腳本GameDataManager。

然后我們打開GameDataManager腳本進(jìn)行如下的編輯:

編寫上圖中的代碼后,我們回到Unity編輯器,然后運(yùn)行,之后我們便會(huì)發(fā)現(xiàn)Project視窗中多了一個(gè)名為ZuiData的文件,如下圖:

然后我們打開ZuiData文件,就會(huì)發(fā)現(xiàn)里面保存著_myPlayer對(duì)象的數(shù)據(jù),如下圖:

到此,我們學(xué)會(huì)了將游戲數(shù)據(jù)序列化并寫入到文件中的操作了。
四、實(shí)現(xiàn)XmlManager的反序列化和讀取操作
學(xué)會(huì)了保存數(shù)據(jù)后,下一步我們就要來實(shí)現(xiàn)讀取文件數(shù)據(jù),并且將其反序列化,成為我們可以使用的對(duì)象。
我們?cè)俅未蜷_XmlManager腳本,新增以下代碼:

- 首先我們看deserializeObject方法,我們通過傳入的參數(shù)ty,確定XmlSerializer需要反序列化的類型,然后需要反序列化的內(nèi)容從string轉(zhuǎn)換成byte[]類型,最后調(diào)用xs.Deserialize方法進(jìn)行反序列化操作,并返回其結(jié)果。
- 接著我們看loadXML方法,有一個(gè)參數(shù),是需要讀取的文件名稱,然后創(chuàng)建一個(gè)StreamReader類型的對(duì)象,然后調(diào)用其方法ReadToEnd進(jìn)行讀取操作,最后返回讀取的內(nèi)容
- stringToUTF8ByteArray和hasFile就很好理解了,一個(gè)是將string類型裝換成byte[]類型,一個(gè)是通過文件名判斷該文件是否存在。
五、實(shí)現(xiàn)GameDataManager的讀取數(shù)據(jù)操作
實(shí)現(xiàn)了XmlManager的反序列化和讀取操作后,我們就可以在GameDataManager中實(shí)現(xiàn)將xml的數(shù)據(jù)讀取,并且把這些數(shù)據(jù)轉(zhuǎn)換成我們需要使用的類型,比如轉(zhuǎn)換成我們_myPlayer的信息。
接下來,我們打開GameDataManager腳本,并新增以下代碼:

- 首先在load方法中,先獲取文件存儲(chǔ)的路徑,然后判斷文件是否存在,如果不存在,則在后臺(tái)打印提示信息,接下來,調(diào)用xm.loadXML方法讀取文件中的數(shù)據(jù),讀取出來的數(shù)據(jù)是一段字符串,然后我們?cè)谡{(diào)用xm.deserializeObject方法把數(shù)據(jù)轉(zhuǎn)換成MyPlayer類型的數(shù)據(jù),最后如果數(shù)據(jù)不為空,我們就把這些數(shù)據(jù)賦值給_myPlayer對(duì)象。
- pressLoadButton方法是后面我們制作讀取按鈕時(shí)會(huì)用的方法,里面主要做了兩件事情,一是調(diào)用load方法,讀取數(shù)據(jù),二是調(diào)用printData方法打印_myPlayer的部分屬性。
- printData方法中,我們調(diào)用Debug.log方法打印出我們想要看的_myPlayer的屬性,而這里我們打印的屬性,是后面我們修改過具體內(nèi)容的幾個(gè)數(shù)據(jù),打印出來就是為了查看是否修改成功。
制作讀取按鈕
有了上面這些方法后,為了在實(shí)際演示中,讓我們可以看到數(shù)據(jù)的讀取后的改變,我們?cè)陧?xiàng)目中新建一個(gè)按鈕,來觸發(fā)數(shù)據(jù)讀取的操作。

我們?cè)陧?xiàng)目中新建一個(gè)名為“LoadButton”的按鈕,然后將其內(nèi)容改為“讀取數(shù)據(jù)”,然后我們?cè)诎粹o的點(diǎn)擊邏輯上掛載GameDataManager中的pressLoadButton方法,如下圖:

實(shí)現(xiàn)按鈕后,我們打開ZuiData文件,將玩家的ID改為99,玩家名字改為“ZuiPlayer”,武器的名稱改為“Eagles”,如下圖:

修改之后,我們?cè)俅未蜷_GameDataManager腳本,把Start方法中的內(nèi)容全部注釋掉,如下圖:

最后我們回到Unity編輯器,然后運(yùn)行程序,點(diǎn)擊讀取數(shù)據(jù)按鈕,然后查看后臺(tái)打印出來的數(shù)據(jù)是否與我們修改過后的數(shù)據(jù)一致,如無意外,效果如下圖:

到此,我們學(xué)會(huì)了XML數(shù)據(jù)的讀取和反序列化。
六、對(duì)文件數(shù)據(jù)進(jìn)行加密和解密
雖然我們現(xiàn)在學(xué)會(huì)了使用xml進(jìn)行數(shù)據(jù)的存儲(chǔ)和讀取,但是就想我們上面讀取操作時(shí)一樣,我們可以直接通過改寫ZuiData文件里面的內(nèi)容,從而改變游戲的數(shù)據(jù),這樣對(duì)于游戲數(shù)據(jù)來說是很不安全的,所以我們最好對(duì)游戲最終保存的數(shù)據(jù)進(jìn)行一些加密操作,這樣就無法通過文件直接修改游戲的數(shù)據(jù)了。
我們?cè)俅未蜷_XmlManager腳本,然后新增以下代碼:

- 首先我們?cè)谇懊嬉肓诵碌念^文件System.Security.Cryptography,我們下面需要用的RijndaelManaged和ICryptTransform類,都是屬于其中。
- 接下來我們定義了我們加密和解密所需要用的密鑰,具體的數(shù)字可以自定義,但是必須一共是32位。
- 然后我們先跳到上圖最后的getRijndaelManaged方法,這里面我們主要是創(chuàng)建并定義我們加密和解密的方式,我們定義了一個(gè)RijndaelManaged對(duì)象,然后設(shè)置密鑰為_keyArray(也就是我們之前定義的密鑰),然后設(shè)置對(duì)稱解密算法的運(yùn)算模式和填充模式(關(guān)于運(yùn)算模式和填充模式的詳細(xì)說明,大家可以參見最后面的參考鏈接),最后我們返回RijndaelManaged的實(shí)例化對(duì)象。
- 接下來我們看到encrypt方法,這里面我們主要是把傳入的數(shù)據(jù),進(jìn)行加密操作,然后返回加密后的數(shù)據(jù),首先我們定義了一個(gè)ICryptTransform類型的對(duì)象,并且他是加密模式的,然后我們把需要加密的數(shù)據(jù)類型轉(zhuǎn)換成byte[]類型,調(diào)用TransformFinalBlock方法得到加密后的數(shù)據(jù),最后將這個(gè)數(shù)據(jù)轉(zhuǎn)換成string類型并返回。
- 最后我們來看decrypt方法,這里我們主要把傳入的數(shù)據(jù),進(jìn)行解密操作,然后返回解密后的數(shù)據(jù),首先我們定義了一個(gè)ICryptTransform類型的對(duì)象,并且他是解密模式的,然后我們把需要解密的數(shù)據(jù)轉(zhuǎn)換成byte[]類型,調(diào)用TransformFinalBlock方法獲得解密后的數(shù)據(jù),最后將這個(gè)數(shù)據(jù)轉(zhuǎn)換成string類型并返回。
完成了這些主要的方法后,我們還需要對(duì)XmlManager腳本進(jìn)行一些小修改,具體見下圖:

做完這些改動(dòng)后,我們?cè)侔?strong>GameDataManager腳本中的Start方法中的代碼注釋取消掉,讓其恢復(fù)作用,最后我們回到Unity編輯器,運(yùn)行程序,可以看到我們save和load操作都是正常運(yùn)行的,而這個(gè)時(shí)候我們?cè)俅未蜷_ZuiData文件,就會(huì)發(fā)現(xiàn)文件中的內(nèi)容變成了一些亂碼,這樣就無法通過修改存檔來改變游戲的數(shù)據(jù)了。

參考閱讀
MSDN 關(guān)于RijndaelManaged類的說明
MSDN 關(guān)于ICryptoTransform類的說明
補(bǔ)充內(nèi)容
- 需要序列化的成員必須是public的,私有的是不會(huì)被序列化的,如果某個(gè)成員變量不想序列化,有兩種方法,一是設(shè)置為私有,二是使用[XmlIgnore]修飾,如下圖:

- 如果一個(gè)成員變量的值為null,序列化時(shí)不會(huì)記錄任何信息。
- 自定義序列化類,必須要有默認(rèn)的構(gòu)造函數(shù)(即不帶任何參數(shù)的構(gòu)造函數(shù)),否則會(huì)報(bào)錯(cuò)。
作者:Zui
鏈接:http://www.itdecent.cn/p/29e3b50deac1
來源:簡(jiǎn)書
簡(jiǎn)書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處。