[C#/Unity] 以《Endless End》為例聊一下怎麼做一個簡單的存讀檔系統

        存檔讀檔可以說是絕大部分遊戲都必備的一個功能、記錄玩家遊戲進度、避免玩家重複體驗進度之前的內容,從而使玩家可以一步一步的靠近終點。而在Unity中,要做一個簡單的存讀檔系統也非常方便,最常見的手段有兩種:

  1. PlayerPrefs
  2. Json

        PlayerPrefs通過它的SetInt/SetFloat/SetString方法創建一個鍵值對,並保存到不同平台對應的地址中(具體位置可參考官方文檔);然後當需要使用這些數據時,就通過PlayerPrefs.GetInt/GetFloat/GetString方法,再通過鍵值得到對應的數據。

        不過PlayerPrefs的缺點還是比較明顯的。沒錯,PlayerPrefs的確是最簡單的,它也確實可以用來保存一些信息,但是不建議用來保存玩家的遊戲進度這類複雜而又敏感的信息的。依我認為,PlayerPrefs起碼有以下幾個缺陷:

        1. 第一,也是最大的缺陷,就是PlayerPrefs默認只能保存Int、Float、String類型的數據;而且每個數據要保存時,都需要單獨調用一次PlayerPrefs。以《Endless End》裡的存儲數據為例:

        可以看到,我除了需要保存一些像float、int這類的值類型數據外,還需要保存一些列表的數據。而這種類型的數據是無法保存到PlayerPrefs中的,如果要強行用PlayerPrefs來保存,就只能為列表中的每一個可能元素需要保存的時候創建一個鍵值對,對於每條元素再單獨調用PlayerPrefs.Set;而讀檔時又要讀到每一條數據的鍵,非常麻煩。

        2.  另外一個則是加密的問題,不過這倒不是一個最核心的問題,畢竟除了用2進制來保存數據的方法外,其他所有的保存方法也需要進行加密處理。而PlayerPrefs本身也是以明文(plaintext)保存的,因此,只要玩家找到對應的地址,進一步就很容易得到數據。

        因此,比起要保存重要的數據,PlayerPrefs更適合保存一些遊戲的設置和狀態,正如它的命名(PlayerPrefs => Player Preference 玩家偏好),比如:音量音效的開關、語言的選擇、按鍵的設置等等。

        Json保存法本質是將一個可序列化的數據類裡內容轉換成一個以Json格式記錄的txt文檔,並保存到開發者選擇的一個指定路徑中;而當玩家需要讀取數據時,就從保存路徑中嘗試得到這個Json格式的txt文檔(如果得不到則代表沒有保存數據),並將裡面的數據寫到對應的數據字段裡,讓遊戲去使用這些數據。

        Json在Unity中的應用主要有兩種方式:一是使用Unity內置的JsonUtility;二是引入其他團隊/個人製作的dll/代碼,比如LitJson/MiniJSON等等,網絡上有很多不同的Json轉譯工具。不過於我而言,我一般都是直接使用Unity內置的JsonUtility來完成,下面我就總結一下我是怎麼在《Endless End》中使用JsonUtility來完成存檔讀檔系統的。

  • 存檔
    • 創建一個數據模型,裡面保存著遊戲所有要保存的數據的字段,不同於使用PlayerPrefs,這些數據的數據類型基本都是沒有限制的,只要數據是可以序列化的(System.Serializable),數據就可以被轉換成Json。
    • 這個數據類繼承了ScriptableObject,添加了CreateAssetMenu的特性。因為這個數據類除了是一個「數據模型」,也是一個可以直接被玩家使用其字段的「Asset」。
      • 因此,這個數據類還需要一些對這些字段的操作方法,比如把數據重置為默認值。
    • 然後保存就相當簡單了
      • 指定一個路徑作為JSON的存儲和讀取地址
      • 將GameData這個類通過JsonUtility.ToJson轉換成字符串
      • 然後將這個字符串通過File.WriteAllText寫到一個json文檔裡共導出至前面指定好的路徑。
    • 最後,當玩家觸發我設計好的保存點時,一個包含了數據模型中所有字段的json文檔就會被輸出到我指定的路徑中。在《Endless End》中,我所指定的路徑是StreamingAsset文件夾(Application.streamingAssestsPath)。刷新一下就能在文件夾中看到一個名為GameSave.json的文檔。裡面的內容如下圖:
  • 讀檔
      • 首先檢查一下路徑和GameSave.json檔案是否存在,如果不存在,代表沒有存檔記錄。
      • 如果存在,就先讀取GameSave.json檔案裡的所有字符串。然後聲明一個GameData的類。
        • 嘗試得到GameSave的ScriptableObject是否已經存在於Assets對應的路徑中,將這個ScriptbleObject賦值給GameData。
          • 如果不存在,就通過ScriptableObject.CreateInstance來創建一個並賦值給GameData。
      • 最後將GameSave.json裡讀取出來的字符串通過JsonUtility.FromJsonOverWrite覆寫到GameData上,再當前的存檔currSave設置為這個從Json文檔讀取出來的GameData。
      • *如果無法讀取到存檔Json,就使用默認的GameData數據(defaultData)
      • 然後玩家以及場景其他受進度所影響的元素就會根據這個currSave來得到數據,並完成符合進度的初始化。
  • 要注意的是,在我的代碼中,AssetDatabase的相關代碼都被注釋掉了。AssetDatabase是在UnityEditor namespace下的類,這使我們可以更方便地去調試並即時在Editor上看到我們修改的內容。但是Editor的東西在專案導出時並不會被打包,因此,如果在不注釋AssetDatabase相關內容時打包將會導致打包出錯。

        存檔讀檔系統本身並不難做,重要的是要搞清楚怎麼將存檔讀檔系統應用到自己的遊戲設計上,簡單來說就是要知道保存進度的時機,有甚麼是要保存的,尤其是自動保存的遊戲。撿東西時要不要保存、解開機關後,如果還沒有到達下一個保存點就死了,那解開了的機關又要不要保存起來等等。這些設計層面上的考慮比存讀檔本身的實現更重要,畢竟存讀檔系統具體要怎麼設計,還是得根據具體的設計需求來考慮。