1. 程式人生 > >Unity - 存讀檔機制簡析

Unity - 存讀檔機制簡析

本文旨在於簡要分析Unity中的兩種存檔機制,即:PlayerPrefs資料持久化方法及Serialization資料序列化方法

較比與源專案,我另加了JSON方法、XML方法等及一些Unity設定,更便於讀者在使用中理解Unity的存檔機制。核心指令碼為Game.cs

  • 源專案地址:How to Save and Load a Game in Unity - raywenderlich
  • 個人專案地址:BattleSave - SouthBegonia


一、PlayerPrefs 資料持久化方法

  1. 儲存原理:採用鍵值對(key與value)的方法,將遊戲資料儲存到本地,是一種Unity自帶的儲存方法。
  2. 儲存型別:僅支援int、float、string三種
  3. 儲存地址:詳見官方文件 PlayerPrefs - Unity Documentation
  4. 讀寫示例:
//專案內未展示該用法,但以下程式碼即為常規用法
//新建存檔
PlayerPrefs.SetInt("Score", 20);
PlayerPrefs.SetFloat("Health", 100.0F);
PlayerPrefs.SetString("Name",m_PlayerName);

//檢驗存檔資訊
if(!PlayerPrefs.HasKey("Name"))
    return;

//讀取存檔
socre = PlayerPrefs.GetInt("Score");
health = PlayerPrefs.GetFloat("Health");
m_PlayerName = PlayerPrefs.GetString("Name");

//刪除存檔
PlayerPrefs.DeleteKey("Score");
  • 優缺點:雖然以這種方式儲存遊戲資料方便快捷,但是當資料量龐大以後,鍵值對的大量建立使用,不僅指令碼控制繁瑣,也有可能造成資源的浪費。因此,只建議對一些基礎資料,例如影象設定、聲音設定等採用該方法儲存。

二、Serialization 序列化方法

  1. 儲存原理:將物件(Object)轉換為資料流(stream of bytes),再經過檔案流儲存到本地的方法。
    • 物件(Object):可以是Unity中的任何檔案或是指令碼
    • 資料流(stream of bytes):
  2. 序列化反序列化:
    • Serialization:物件-->資料流
    • Deserialization:資料流-->物件
  3. 序列化的方法:
    • 二進位制方法
    • JSON方法
    • XML方法

1. 二進位制儲存(Binary Formatter):

//存檔資訊的類:
[System.Serializable]
public class Save
{
    public int hits = 0;
    public int shots = 0;
    public List<int> livingTargetPositions = new List<int>();
    public List<int> livingTargetsTypes = new List<int>();
}

//設定遊戲數值
public void SetGame(Save save)
{
    hits = save.hits;
    shots = save.shots;

    for (int i = 0; i < save.livingTargetPositions.Count; i++)
    {
        int position = save.livingTargetPositions[i];
        Target target = targets[position].GetComponent<Target>();
        target.ActivateRobot((RobotTypes)save.livingTargetsTypes[i]);
        target.GetComponent<Target>().ResetDeathTimer();
    }
}

//存檔函式:
public void SaveGame()
{
    //1. 序列化過程
    //建立save物件儲存遊戲資訊
    Save save = CreateSaveGameObject();
    string filePath = Application.dataPath + "/gameSaveBySerialize.save";

    //2. 建立二進位制格式化程式及檔案流
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = File.Create(filePath);

    //3. 將save物件序列化到file流
    bf.Serialize(file, save);
    file.Close();
}

//讀檔函式:
public void LoadGame()
{
    string filePath = Application.dataPath + "/gameSaveBySerialize.save";

    //1. 檢驗目標位置是否有存檔
    if (File.Exists(filePath))
    {
        //2. 建立二進位制格式化程式,開啟檔案流
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(filePath, FileMode.Open);

        //3. 將file流反序列化到save物件      
        Save save = (Save)bf.Deserialize(file);
        file.Close();

        //從save物件讀取資訊到本地
        SetGame(save);
    }
    else  
        Debug.Log("No gamesaved!"); 
}

2. JSON方法:

/*
 * 注意:使用JSON存檔方法需要用到LitJson庫,LitJson.dll檔案可在專案Assets目錄下找到。
 * 使用方法:將LitJson.dll拖拽到個人專案Assets目錄下即可
*/

//JSON存檔函式:
public void SaveAsJson()
{   
    //1. 建立save物件儲存遊戲資訊
    Save save = CreateSaveGameobject();
    string path = Application.dataPath + "/gameSaveByJson.json";

    //2. 利用JsonMapper將save物件轉換為Json格式的字串
    string saveJsonStr = JsonMapper.ToJson(save);

    //3. 建立StreamWriter,將Json字串寫入檔案中
    StreamWriter sw = new StreamWriter(path);
    sw.Write(saveJsonStr);
    sw.Close();
}

//JSON讀檔函式:
public void LoadAsJson()
{ 
    string path = Application.dataPath + "/gameSaveByJson.json";

    //1. 檢驗目標位置是否有存檔
    if(File.Exists(path))
    {
        //2. 建立一個StreamReader,用來讀取流
        StreamReader sr = new StreamReader(path);

        //3. 將讀取到的流賦值給jsonStr
        string jsonStr = sr.ReadToEnd();
        sr.Close();

        //4. 將字串jsonStr轉換為Save物件
        Save save = JsonMapper.ToObject<Save>(jsonStr);
        
        //從save物件讀取資訊到本地
        SetGame(save);
    }
    else
        Debug.Log("No gamesaved!"); 
}

JSON存檔格式:

{
    "livingTargetPositions":[0,1,2,4],
    "livingTargetsTypes":[2,2,2,1],
    "hits":1,
    "shots":8
}

3. XML方法:

//XML儲存
public void SaveAsXml()
{
    Save save = CreateSaveGameObject();

    //建立XML檔案的儲存路徑
    string filePath = Application.dataPath + "/gameSaveByXML.txt";

    //建立XML文件
    XmlDocument xmlDoc = new XmlDocument();

    //建立根節點,即最上層節點
    XmlElement root = xmlDoc.CreateElement("save");

    //設定根節點中的值
    root.SetAttribute("name", "saveFile1");

    //建立XmlElement
    XmlElement target;
    XmlElement targetPosition;
    XmlElement targetType;

    //遍歷save中儲存的資料,將資料轉換成XML格式
    for (int i = 0; i < save.livingTargetPositions.Count; i++)
    {
        target = xmlDoc.CreateElement("target");
        targetPosition = xmlDoc.CreateElement("targetPosition");

        //設定InnerText值
        targetPosition.InnerText = save.livingTargetPositions[i].ToString();
        targetType = xmlDoc.CreateElement("targetType");
        targetType.InnerText = save.livingTargetsTypes[i].ToString();

        //設定節點間的層級關係 root -- target -- (targetPosition, monsterType)
        target.AppendChild(targetPosition);
        target.AppendChild(targetType);
        root.AppendChild(target);
    }

    //設定射擊數和分數節點並設定層級關係
    XmlElement shots = xmlDoc.CreateElement("shoots");
    shots.InnerText = save.shots.ToString();
    root.AppendChild(shots);

    XmlElement hits = xmlDoc.CreateElement("hits");
    hits.InnerText = save.hits.ToString();
    root.AppendChild(hits);

    xmlDoc.AppendChild(root);
    xmlDoc.Save(filePath);

    if (File.Exists(Application.dataPath + "/gameSaveByXML.txt"))
    {
        Debug.Log("Saving as XML");
    }
}

//XML讀取
public void LoadAsXml()
{
    string filePath = Application.dataPath + "/gameSaveByXML.txt";
    if (File.Exists(filePath))
    {
        Save save = new Save();

        //載入XML文件
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(filePath);

        //通過節點名稱來獲取元素,結果為XmlNodeList型別
        XmlNodeList targets = xmlDoc.GetElementsByTagName("target");

        //遍歷所有的target節點,並獲得子節點和子節點的InnerText
        if (targets.Count != 0)
        {
            foreach (XmlNode target in targets)
            {
                    //把得到的值儲存到save中
                XmlNode targetPosition = target.ChildNodes[0];
                int targetPositionIndex = int.Parse(targetPosition.InnerText);                    
                save.livingTargetPositions.Add(targetPositionIndex);

                XmlNode targetType = target.ChildNodes[1];
                int targetTypeIndex = int.Parse(targetType.InnerText);
                save.livingTargetsTypes.Add(targetTypeIndex);
            }
        }

        //得到儲存的射擊數和分數
        XmlNodeList shoots = xmlDoc.GetElementsByTagName("shoots");
        int shootNumCount = int.Parse(shoots[0].InnerText);
        save.shots = shootNumCount;

        XmlNodeList hits = xmlDoc.GetElementsByTagName("hits");
        int hitsCount = int.Parse(hits[0].InnerText);
        save.hits = hitsCount;

        SetGame(save);
    }
    else
    {
        Debug.Log("No game saved!");
    }
}

XML存檔格式:

<save name="saveFile1">
  <target>
    <targetPosition>0</targetPosition>
    <targetType>2</targetType>
  </target>
  <target>
    <targetPosition>1</targetPosition>
    <targetType>2</targetType>
  </target>
  <target>
    <targetPosition>2</targetPosition>
    <targetType>2</targetType>
  </target>
  <target>
    <targetPosition>3</targetPosition>
    <targetType>2</targetType>
  </target>
  <shoots>13</shoots>
  <hits>3</hits>
</save>

三、總述

無論是資料持久化方法還是序列化方法都可以實現Unity的存檔機制。資料持久化方法操作方便,適用於數值較少的小專案。序列化方法的存檔格式較為規範,其中二進位制方法操作簡單,但可讀性差;JSON方法存檔格式規範易讀,具有一定的可讀性;XML方法操作繁瑣,但是存檔格式可讀性強,JSON和XML存檔都可以用文字讀取便於檢視。
綜上所述,Unity存檔機制眾多,但還應按照個人專案需求選擇合適的存檔方法。


四、參考

  • PlayerPrefs - Unity Documentation
  • How to Save and Load a Game in Unity - raywenderlich
  • 對於PlayerPrefs學習以及儲存的研究 - 果vinegar
  • Save&Load Unity存檔讀檔的學習總結 - JoharWong
  • C#中File和FileStream的用法 - 憶汐辰
  • Application.dataPath - Unity Documentation