Unity開發(fā)中異步加載配置文件驯鳖,像讀取數(shù)據(jù)庫一樣讀取配置信息

數(shù)據(jù)驅(qū)動

圖片來源于國產(chǎn)單機游戲古劍奇譚

數(shù)據(jù)驅(qū)動是軟件設(shè)計與開發(fā)中不可忽視的內(nèi)容闲询,開發(fā)電子游戲更是如此。電子游戲世界是由邏輯與數(shù)據(jù)構(gòu)建的浅辙。在開發(fā)過程中扭弧,我們基本上會將邏輯與數(shù)據(jù)分離開來。游戲開發(fā)完成后记舆,邏輯部分相對改動較小鸽捻,而數(shù)據(jù)的改動則相對頻繁。我們可能需要不斷修改來讓游戲世界達(dá)到平衡泽腮。因此御蒲,在游戲開發(fā)按需加載配置是一項很重要的任務(wù)。

CSV文件

使用逗號分隔值(Comma-Separated Values诊赊,CSV厚满,有時也稱為字符分隔值,因為分隔字符也可以不是逗號)作為配置數(shù)據(jù)的手段是較為常見的碧磅。CSV讀取方便又能使用Excel進(jìn)行編輯碘箍,作為游戲中的配置文件十分合適。隨著游戲項目的進(jìn)展续崖,配置表將越來越多敲街,表的內(nèi)容也會越來越多,合理的加載這些文件顯得尤為重要严望。本文將介紹如何在Unity中異步加載CSV文件并方便讀取其中的數(shù)據(jù)多艇。

實踐

在開始之前,我想指出筆者使用的Unity版本是5.3.4像吻。對本文的內(nèi)容來說這可能無關(guān)緊要峻黍,但若有讀者想要參考全部代碼,并運行demo拨匆,可以前往我的gihub(請切換到csvhelper這個分支)姆涩。如果你克隆了我的庫,知道我使用哪個版本的Unity可能對你有所幫助惭每。
我們首先來設(shè)計存儲表中數(shù)據(jù)的類骨饿。

public class CSVLine : IEnumerable
{
    private Dictionary<string,string> dataContainer = new Dictionary<string,string>();

    private void AddItem(string key,string value){
        if(dataContainer.ContainsKey(key)){
            Debug.LogError(string.Format("CSVLine AddItem: there is a same key you want to add. key = {0}", key));
        }else{
            dataContainer.Add(key,value);
        }
    }

    public string this[string key]{
        get { return dataContainer[key]; }
        set { AddItem(key, value); }
    }

    public IEnumerator GetEnumerator()
    {
        foreach (KeyValuePair<string,string> item in dataContainer)
        {
            yield return item;
        }
    }
}

CSVLine類將用來存儲表中每一行的數(shù)據(jù)亏栈。

public class CSVTable : IEnumerable
{
    private Dictionary<string, CSVLine> dataContainer = new Dictionary<string, CSVLine>();

    private void AddLine(string key, CSVLine line)
    {
        if(dataContainer.ContainsKey(key)){
            Debug.LogError(string.Format("CSVTable AddLine: there is a same key you want to add. key = {0}", key));
        }else{
            dataContainer.Add(key, line);
        }
    }
    public CSVLine this[string key]
    {
        get { return dataContainer[key]; }
        set { AddLine(key, value); }
    }

    public IEnumerator GetEnumerator()
    {
        foreach (var item in dataContainer)
        {
            yield return item.Value;
        }
    }

    public CSVLine WhereIDEquals(int id)
    {
        CSVLine result = null;
        if (!dataContainer.TryGetValue(id.ToString(), out result))
        {
            Debug.LogError(string.Format("CSVTable WhereIDEquals: The line you want to get data from is not found. id:{0}", id));
        }
        return result;
    }
}

CSVTable用來存儲每行的主鍵及行內(nèi)容的引用。

public delegate void ReadCSVFinished(CSVTable result);

public class CSVHelper : MonoBehaviour
{
    #region singleton
    private static GameObject container = null;
    private static CSVHelper instance = null;
    public static CSVHelper Instance()
    {
        if (instance == null)
        {
            container = new GameObject("CSVHelper");
            instance = container.AddComponent<CSVHelper>();
        }
        return instance;
    }
    #endregion

    #region mono
    void Awake()
    {
        DontDestroyOnLoad(container);
    }
    #endregion

    #region private members
    //不同平臺下StreamingAssets的路徑是不同的宏赘,這里需要注意一下绒北。
    public static readonly string csvFilePath =
    #if UNITY_ANDROID
            "jar:file://" + Application.dataPath + "!/assets/";
    #elif UNITY_IPHONE
            Application.dataPath + "/Raw/";
    #elif UNITY_STANDALONE_WIN || UNITY_EDITOR
            "file://" + Application.dataPath + "/StreamingAssets/";
    #else
            string.Empty;
    #endif
    private Dictionary<string, CSVTable> readedTable = null;
    #endregion

    #region public interfaces
    public void ReadCSVFile(string fileName, ReadCSVFinished callback)
    {
        
        if (readedTable == null)
            readedTable = new Dictionary<string, CSVTable>();
        CSVTable result;
        if (readedTable.TryGetValue(fileName, out result))
        {
            Debug.LogWarning(string.Format("CSVHelper ReadCSVFile: You already read the file:{0}", fileName));
            return;
        }
        StartCoroutine(LoadCSVCoroutine(fileName, callback));
    }

    public CSVTable SelectFrom(string tableName)
    {
        CSVTable result = null;
        if (!readedTable.TryGetValue(tableName, out result))
        {
            Debug.LogError(string.Format("CSVHelper SelectFrom: The table you want to get data from is not readed. table name:{0}",tableName));
        }
        return result;
    }
    #endregion

    #region private imp
    private IEnumerator LoadCSVCoroutine(string fileName, ReadCSVFinished callback)
    {
        string fileFullName = csvFilePath + fileName + ".csv";
        using (WWW www = new WWW(fileFullName))
        {
            yield return www;
            string text = string.Empty;
            if (!string.IsNullOrEmpty(www.error))
            {
                Debug.LogError(string.Format("CSVHelper LoadCSVCoroutine:Load file failed file = {0}, error message = {1}", fileFullName, www.error));
                yield break;
            }
            text = www.text;
            if (string.IsNullOrEmpty(text))
            {
                Debug.LogError(string.Format("CSVHelper LoadCSVCoroutine:Loaded file is empty file = {0}", fileFullName));
                yield break;
            }
            CSVTable table = ReadTextToCSVTable(text);
            readedTable.Add(fileName, table);
            if (callback != null)
            {
                callback.Invoke(table);
            }
        }
    }

    private CSVTable ReadTextToCSVTable(string text)
    {
        CSVTable result = new CSVTable();
        text = text.Replace("\r", "");
        string[] lines = text.Split('\n');
        if (lines.Length < 2)
        {
            Debug.LogError("CSVHelper ReadTextToCSVData: Loaded text is not csv format");//必需包含一行鍵,一行值察署,至少兩行
        }
        string[] keys = lines[0].Split(',');//第一行是鍵
        for (int i = 1; i < lines.Length; i++)//第二行開始是值
        {
            CSVLine curLine = new CSVLine();
            string line = lines[i];
            if (string.IsNullOrEmpty(line.Trim()))//略過空行
            {
                break;
            }
            string[] items = line.Split(',');
            string key = items[0].Trim();//每一行的第一個值是唯一標(biāo)識符
            for (int j = 0; j < items.Length; j++)
            {
                string item = items[j].Trim();
                curLine[keys[j]] = item;
            }
            result[key] = curLine;
        }
        return result;
    }
    #endregion
}

接著是我們的CSVReader類闷游。這是一個mono的單例類,因為我使用了Unity實現(xiàn)的協(xié)程來做異步贴汪。通過ReadCSVFile接口來加載文件脐往,加載完成后使用一個ReadCSVFinished 的回調(diào)函數(shù)獲取加載好的數(shù)據(jù)。解析文件的細(xì)節(jié)在ReadTextToCSVTable函數(shù)中扳埂。
下面我們來看具體的使用:
在StreamAssets文件夾下準(zhǔn)備一個測試用的csv文件业簿,我的文件如下:



在場景中任意游戲物體中掛載以下腳本:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ReadTest : MonoBehaviour
{
    private bool readFinish = false;
    void Start()
    {
        CSVHelper.Instance().ReadCSVFile("csv_test", (table) => {
            readFinish = true;
            // 可以遍歷整張表
            foreach (CSVLine line in table)
            {
                foreach (KeyValuePair<string,string> item in line)
                {
                    Debug.Log(string.Format("item key = {0} item value = {1}", item.Key, item.Value));
                }
            }
            //可以拿到表中任意一項數(shù)據(jù)
            Debug.Log(table["10011"]["id"]);
        });
    }

    void Update()
    {
        if (readFinish)
        {
            // 可以類似訪問數(shù)據(jù)庫一樣訪問配置表中的數(shù)據(jù)
            CSVLine line = CSVHelper.Instance().SelectFrom("csv_test").WhereIDEquals(10011);
            Debug.Log(line["name"]);
            readFinish = false;
        }
    }
}

運行游戲可以查看效果:


結(jié)語

本文介紹的方法實現(xiàn)了異步加載配置文件,并且可以像讀取數(shù)據(jù)庫一樣讀取數(shù)據(jù)阳懂。當(dāng)然辖源,要完全像使用SQL語句那樣簡便并且實現(xiàn)數(shù)據(jù)的各種組合比較困難,這里是夸張的說法希太。但對于讀取配置信息已經(jīng)足夠克饶。我希望讀者可以實現(xiàn)自己的擴(kuò)展,使讀取數(shù)據(jù)更容易誊辉。如果有興趣的話矾湃,也可以實現(xiàn)一個CSVWriter類用于數(shù)據(jù)寫入。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堕澄,一起剝皮案震驚了整個濱河市邀跃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蛙紫,老刑警劉巖拍屑,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坑傅,居然都是意外死亡僵驰,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門唁毒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒜茴,“玉大人,你說我怎么就攤上這事浆西》鬯剑” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵近零,是天一觀的道長诺核。 經(jīng)常有香客問我抄肖,道長,這世上最難降的妖魔是什么窖杀? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任憎瘸,我火速辦了婚禮,結(jié)果婚禮上陈瘦,老公的妹妹穿的比我還像新娘。我一直安慰自己潮售,他們只是感情好痊项,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著酥诽,像睡著了一般鞍泉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肮帐,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天咖驮,我揣著相機與錄音,去河邊找鬼训枢。 笑死托修,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恒界。 我是一名探鬼主播睦刃,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼十酣!你這毒婦竟也來了涩拙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤耸采,失蹤者是張志新(化名)和其女友劉穎兴泥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虾宇,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡搓彻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了嘱朽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片好唯。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖燥翅,靈堂內(nèi)的尸體忽然破棺而出骑篙,到底是詐尸還是另有隱情,我是刑警寧澤森书,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布靶端,位于F島的核電站谎势,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏杨名。R本人自食惡果不足惜脏榆,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望台谍。 院中可真熱鬧须喂,春花似錦、人聲如沸趁蕊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽掷伙。三九已至是己,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間任柜,已是汗流浹背卒废。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留宙地,地道東北人摔认。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像宅粥,于是被迫代替她去往敵國和親级野。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容