2020年1月1日 星期三

Unity筆記:存檔系統


----------
2020/6/26
----------
花了一個早上把小地圖王國的存檔系統給做完了,
很多人大概覺得遊戲的存檔系統只要按個鍵什麼的就會生出來了,
但實際做過遊戲的人都知道存檔其實是開發中前幾麻煩的事情,
像Unity這樣的引擎,它的預設類還是沒有辦法直接序列化的,
所以必須把資料類和引擎類分開,
在開發場外人生時我就已經痛苦過一次了,
這次開發小地圖王國更多的是上百個獨立單位和腳本,
存檔系統也會稍微複雜一點,
話是這麼說,不過有了之前的經驗,
只掙扎了一個早上就完整做出來了,
唉,人的成長有時候就是這樣,
努力著努力著就熟練了,
最後放棄思考也能把它做完,
效率也蠻高的,
測試上百次最後看到沒問題時應該是很有成就感的,
不過到後面整個人已經有點萎縮起來了,
好累啊,想脫離苦海的感覺,
我真的不是那種喜歡寫程式的人,
這種存檔系統尤其,
不過一回生二回熟,
下次寫這種東西可能就直接複製就好了,
所以把今天寫出來還沒整理又亂七八糟的東西先記在這吧,
至少這是可以完美存檔的。
(つд⊂)
----------
存檔分為幾個部分:

1.固定的存檔:
舉例來說,玩家進了遊戲順手調整了音效,
那麼可以預期下次他打開遊戲時希望系統自動讀取了上次調整的音效,
這部分的"設定"屬於固定的自動存檔,
玩家雖然沒感覺,
但還是要存起來。

2.主腳本的存檔:
包含了遊戲管理員、玩家和世界的資源、
系統項目甚至是玩家物件都放在這裡。

3.遊戲中的物件:
相對複雜的部份,
首先,可以預期遊戲中會有不少動態生成的物件,
像小地圖王國這種建造沙盒類的尤其如此,
生產的單位、建築都屬於這類,
Unity非但沒有那種一鍵儲存物件的功能,
相反的物件資料是不能直接序列化的,
一些關聯性和繼承unity類的腳本都要手動的重新讀取,
資源站上當然有很多號稱可以一鍵讀取的腳本,
但不論如何,到了最後還是要自己寫接口把自己的資料存進去才行,
如果沒有特別的需求,
自己寫說不定還少點麻煩,
至少我只花了一個上午就從零開始做完了ww。
(早上05~11點)

4.地圖和其他東西的存檔:
這些東西不一定能序列化,
資料需要整個重建才行,
稍微有一點點麻煩,
不過量相較起來會少一點。
----------
開始前要意識到一件事情,
像存檔系統這種東西愈早寫完愈好,
如果很晚才寫,
那麼之後要改架構工作量就會成指數成長,
很高興這次有了之前的開發經驗沒有再走回頭路,
直接把專案開始後的基本架構給設計好了,
真的感到自己成長了啊,
QAQ
----------
總之,我把這個上午寫完的東西貼在這裡,
亂七八糟的只留給自己參考,
其實核心只有序列化跟反序列化而已,
真的要做並不困難,
做完之後就一勞永逸了,
每個單位只要調用同一個方法就會自動存檔了。
(つд⊂)
----------
固定存檔:

[System.Serializable]
public class 固定存檔
{
    public Basic 玩家資料二;


    public string 是否有存檔 = "";


    public float 音樂音量存檔 = 1.0f;
    public float 音效音量存檔 = 1.0f;
    public void 儲存音樂音量()
    {
        音樂音量存檔 = 玩家資料二.背景音樂管理員.volume;
        Save();
    }
    public void 儲存音效音量()
    {
        音效音量存檔 = AudioListener.volume;
        Save();
    }
   
   
    public void 讀取音量存檔()
    {
        玩家資料二.調整音樂音量(音樂音量存檔);
        玩家資料二.調整音效音量(音效音量存檔);
    }

    public string 語言模式;

    public void Save()
    {
        玩家資料二 = GameObject.Find("GameManager").GetComponent<Basic>();

        var serializedData = JsonUtility.ToJson(玩家資料二.固定存檔);

        byte[] serizliedData = System.Text.Encoding.UTF8.GetBytes(serializedData);

        var filePath = Application.persistentDataPath + "/save/" + "staticSave.dat";

        Debug.Log("存檔位置:" + filePath);

        System.IO.File.WriteAllBytes(filePath, serizliedData);
    }

    public void Load()
    {
        玩家資料二 = GameObject.Find("GameManager").GetComponent<Basic>();

        string dirPath = Application.persistentDataPath + "/save";
        if (System.IO.Directory.Exists(dirPath))
        {
            Debug.Log("已有存檔資料夾:" + dirPath);
        }
        else
        {
            System.IO.Directory.CreateDirectory(dirPath);
            Debug.Log("創建存檔資料夾:" + dirPath);
        }

        var filePath = Application.persistentDataPath + "/save/" + "staticSave.dat";

        Debug.Log("存檔位置:" + filePath);

        try
        {
            var serizliedData = System.IO.File.ReadAllBytes(filePath);

            var serializedData = System.Text.Encoding.UTF8.GetString(serizliedData);

            JsonUtility.FromJsonOverwrite(serializedData, 玩家資料二.固定存檔);
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.Log("讀檔無存檔文件");
        }

        玩家資料二 = GameObject.Find("GameManager").GetComponent<Basic>();
    }
}
----------
主腳本存檔:

public void Save()
    {
        玩家名 = 玩家資料.玩家基本單位管理員.單位資料.單位名稱;

        玩家x = 玩家資料.player.transform.position.x;
        玩家y = 玩家資料.player.transform.position.y;

        var serializedData = JsonUtility.ToJson(玩家資料.玩家);

        byte[] serizliedData = System.Text.Encoding.UTF8.GetBytes(serializedData);

        var filePath = Application.persistentDataPath + "/save/save1/" + "playerSave.dat";

        Debug.Log("存檔位置:" + filePath);

        System.IO.File.WriteAllBytes(filePath, serizliedData);

   
        //地圖存檔區===============================
        var mapserializedData = JsonUtility.ToJson(new Serialization<string, string>(地塊位置對應地塊名字));

        byte[] mapserizliedData = System.Text.Encoding.UTF8.GetBytes(mapserializedData);

        var mapfilePath = Application.persistentDataPath + "/save/save1/" + "mapSave.dat";

        Debug.Log("地圖存檔位置:" + mapfilePath);

        System.IO.File.WriteAllBytes(mapfilePath, mapserizliedData);
        //===============================

        //單位資料存檔區===============================
        var itemdataserializedData = JsonUtility.ToJson(new Serialization<string, string>(存檔次序對應單位類型));

        byte[] itemdataserizliedData = System.Text.Encoding.UTF8.GetBytes(itemdataserializedData);

        var itemdatafilePath = Application.persistentDataPath + "/save/save1/" + "itemdataSave.dat";

        Debug.Log("單位資料存檔位置:" + itemdatafilePath);

        System.IO.File.WriteAllBytes(itemdatafilePath, itemdataserizliedData);
        //===============================
    }

    public void Load()
    {
        var filePath = Application.persistentDataPath + "/save/save1/" + "playerSave.dat";

        Debug.Log("存檔位置:" + filePath);

        try
        {
            var serizliedData = System.IO.File.ReadAllBytes(filePath);

            var serializedData = System.Text.Encoding.UTF8.GetString(serizliedData);

            JsonUtility.FromJsonOverwrite(serializedData, 玩家資料.玩家);
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.Log("讀檔無存檔文件");
        }

        //地圖讀檔區=====================
        var mapfilePath = Application.persistentDataPath + "/save/save1/" + "mapSave.dat";

        Debug.Log("地圖存檔位置:" + mapfilePath);

        try
        {
            var mapserizliedData = System.IO.File.ReadAllBytes(mapfilePath);

            var mapserializedData = System.Text.Encoding.UTF8.GetString(mapserizliedData);

            地塊位置對應地塊名字 = JsonUtility.FromJson<Serialization<string, string>>(mapserializedData).ToDictionary();
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.Log("讀檔無存檔文件");
        }
        //==============

        //單位資料讀檔區=====================
        var itemdatafilePath = Application.persistentDataPath + "/save/save1/" + "itemdataSave.dat";

        Debug.Log("單位資料存檔位置:" + itemdatafilePath);

        try
        {
            var itemdataserizliedData = System.IO.File.ReadAllBytes(itemdatafilePath);

            var itemdataserializedData = System.Text.Encoding.UTF8.GetString(itemdataserizliedData);

            存檔次序對應單位類型 = JsonUtility.FromJson<Serialization<string, string>>(itemdataserializedData).ToDictionary();
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.Log("讀檔無存檔文件");
        }
        //==============
    }
----------
用到的工具類,
Unity的Json現在應該是可以存list的,
不過泛型不行,
還有字典也不行,
所以要調用以下的工具類來把它存進去,
用到字典的部分包含了地圖的地址對地塊名,
還有所有需存檔物件的存檔編號對應單位名稱,
泛型list應該是沒有用到才對。

// Serialization.cs
// List<T>
[System.Serializable]
public class Serialization<T>
{
    [SerializeField]
    List<T> target;
    public List<T> ToList() { return target; }

    public Serialization(List<T> target)
    {
        this.target = target;
    }
}

// Dictionary<TKey, TValue>
[System.Serializable]
public class Serialization<TKey, TValue> : ISerializationCallbackReceiver
{
    [SerializeField]
    List<TKey> keys;
    [SerializeField]
    List<TValue> values;

    Dictionary<TKey, TValue> target;
    public Dictionary<TKey, TValue> ToDictionary() { return target; }

    public Serialization(Dictionary<TKey, TValue> target)
    {
        this.target = target;
    }

    public void OnBeforeSerialize()
    {
        keys = new List<TKey>(target.Keys);
        values = new List<TValue>(target.Values);
    }

    public void OnAfterDeserialize()
    {
        var count = System.Math.Min(keys.Count, values.Count);
        target = new Dictionary<TKey, TValue>(count);
        for (var i = 0; i < count; ++i)
        {
            target.Add(keys[i], values[i]);
        }
    }
}
----------
單位存檔:

[System.Serializable]
public class 單位資料
{
    public itemBasic 單位資料腳本;

    public float 上次生產單位時間;
    public float 上次工作時間;

    public string 陣營;
    public string 單位細節;
    public long 血量;
    public long 血量上限;
    public float 上次恢復血量時間;
    public long 攻擊力;

    public string 單位名稱;
    public long 單位經驗;

    public float 單位x;
    public float 單位y;

    public void 存檔()
    {
        單位x = 單位資料腳本.gameObject.transform.position.x;
        單位y = 單位資料腳本.gameObject.transform.position.y;

        int 存檔次序 = 單位資料腳本.遊戲管理員.玩家.存檔次序對應單位類型.Count;

        單位資料腳本.遊戲管理員.玩家.存檔次序對應單位類型.Add
            (
            存檔次序.ToString()
            ,
            單位細節
            );

        Save(存檔次序);
    }

    public void Save(int 單位次序)
    {
        var serializedData = JsonUtility.ToJson(單位資料腳本.單位資料);

        byte[] serizliedData = System.Text.Encoding.UTF8.GetBytes(serializedData);

        var filePath = Application.persistentDataPath + "/save/save1/" + 單位次序 + "Save.dat";

        Debug.Log("存檔位置:" + filePath);

        System.IO.File.WriteAllBytes(filePath, serizliedData);
    }

    public void Load(int 單位次序)
    {
        var filePath = Application.persistentDataPath + "/save/save1/" + 單位次序 + "Save.dat";

        Debug.Log("存檔位置:" + filePath);

        try
        {
            var serizliedData = System.IO.File.ReadAllBytes(filePath);

            var serializedData = System.Text.Encoding.UTF8.GetString(serizliedData);

            JsonUtility.FromJsonOverwrite(serializedData, 單位資料腳本.單位資料);
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.Log("讀檔無存檔文件");
        }
    }
}
----------

地圖存檔和最後保存和讀取的接口:

public void 存檔按鈕()
    {
        string dirPath = Application.persistentDataPath + "/save/save1";

        if (System.IO.Directory.Exists(dirPath))
        {
            System.IO.Directory.Delete(dirPath,true);
            Debug.Log("刪除已有存檔資料夾:" + dirPath);

            System.IO.Directory.CreateDirectory(dirPath);
            Debug.Log("創建存檔資料夾:" + dirPath);
        }
        else
        {
            System.IO.Directory.CreateDirectory(dirPath);
            Debug.Log("創建存檔資料夾:" + dirPath);
        }


        GameObject[] 需存檔單位列表 = GameObject.FindGameObjectsWithTag("save");

        玩家.存檔次序對應單位類型 = new Dictionary<string, string>();

        for (int i = 0; i < 需存檔單位列表.Length; i++)
        {
            itemBasic 存檔單位基本管理員 = 玩家基本單位管理員.對象基本腳本(需存檔單位列表[i]);

            存檔單位基本管理員.存檔();
        }

        玩家.Save();

       



        固定存檔.是否有存檔 = 玩家.是();
    }
    public void 讀檔按鈕()
    {
        if(固定存檔.是否有存檔 == 玩家.是())
        {
            初始化();

            玩家.Load();

            玩家基本單位管理員.單位資料.單位名稱 = 玩家.玩家名;

            player.transform.position = new Vector2(玩家.玩家x,玩家.玩家y);

            for (int y = 玩家.最小levelY; y < 玩家.levelY; y++)
            {
                for (int x = 玩家.最小levelX; x < 玩家.levelX; x++)
                {
                    string 位置文字 = 轉換地塊位置為文字(x, y);

                    if (玩家.地塊位置對應地塊名字.ContainsKey(位置文字))
                    {
                        tilemap.SetTile
                    (new Vector3Int((int)轉換地塊文字為位置(位置文字).x, (int)轉換地塊文字為位置(位置文字).y, 0),
                    地塊名字對應地塊物件[玩家.地塊位置對應地塊名字[位置文字]]);
                    }
                }
            }

            for (int i = 0; i < 玩家.存檔次序對應單位類型.Count; i++)
            {
                string key = i.ToString();

                if (玩家.存檔次序對應單位類型.ContainsKey(key))
                {
                    if (玩家.存檔次序對應單位類型[key] == 判定.單位細節玩家劍士.ToString())
                    {
                        GameObject 暫存單位 = 生成基本劍士玩家方法(player.transform.position);

                        對象讀檔(i, 暫存單位);
                    }
                    else if (玩家.存檔次序對應單位類型[key] == 判定.單位細節敵人劍士.ToString())
                    {
                        GameObject 暫存單位 = 生成基本劍士敵人方法(player.transform.position);

                        對象讀檔(i, 暫存單位);
                    }
                    else if (玩家.存檔次序對應單位類型[key] == 判定.單位細節玩家工人.ToString())
                    {
                        GameObject 暫存單位 = 生成基本工人玩家方法(player.transform.position);

                        對象讀檔(i, 暫存單位);
                    }
                    else if (玩家.存檔次序對應單位類型[key] == 判定.單位細節玩家工人房屋.ToString())
                    {
                        GameObject 暫存單位 = 生成基本工人房屋玩家方法(player.transform.position);

                        對象讀檔(i, 暫存單位);
                    }

                }
            }

            更新圖標人口與消耗量();
            更新所有資源();

            詢問名字.SetActive(false);
            主選單第一層.SetActive(true);

            主選單.SetActive(false);
        }
        else
        {
            更新作者的話
                (
                "No saved file found!" + "\n" +
                "沒有找到存檔!" + "\n"
                );
        }
    }

    public void 對象讀檔(int i,GameObject 對象)
    {
        itemBasic 對象基本腳本 = 玩家基本單位管理員.對象基本腳本(對象);

        對象基本腳本.單位資料.Load(i);

        對象基本腳本.gameObject.transform.position =
            new Vector2
            (
                對象基本腳本.單位資料.單位x,
                對象基本腳本.單位資料.單位y
            );

        對象基本腳本.單位資料.單位資料腳本 = 對象基本腳本;
    }
----------

就這樣!
做完了好開心啊,
這幾天可以火力全開來寫新單位了,
我有強烈的預感這會是一款神作,
希望這種感覺能持久一點。
(〃∀〃)ゞ

沒有留言:

張貼留言

你發現了這篇網誌的留言板,在這留點什麼吧|д・)