Unity开发中异步加载配置文件,像读取数据库一样读取配置信息

数据驱动

图片来源于国产单机游戏古剑奇谭

数据驱动是软件设计与开发中不可忽视的内容,开发电子游戏更是如此。电子游戏世界是由逻辑与数据构建的。在开发过程中,我们基本上会将逻辑与数据分离开来。游戏开发完成后,逻辑部分相对改动较小,而数据的改动则相对频繁。我们可能需要不断修改来让游戏世界达到平衡。因此,在游戏开发按需加载配置是一项很重要的任务。

CSV文件

使用逗号分隔值(Comma-Separated Values,CSV,有时也称为字符分隔值,因为分隔字符也可以不是逗号)作为配置数据的手段是较为常见的。CSV读取方便又能使用Excel进行编辑,作为游戏中的配置文件十分合适。随着游戏项目的进展,配置表将越来越多,表的内容也会越来越多,合理的加载这些文件显得尤为重要。本文将介绍如何在Unity中异步加载CSV文件并方便读取其中的数据。

实践

在开始之前,我想指出笔者使用的Unity版本是5.3.4。对本文的内容来说这可能无关紧要,但若有读者想要参考全部代码,并运行demo,可以前往我的gihub(请切换到csvhelper这个分支)。如果你克隆了我的库,知道我使用哪个版本的Unity可能对你有所帮助。
我们首先来设计存储表中数据的类。

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类将用来存储表中每一行的数据。

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用来存储每行的主键及行内容的引用。

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();//每一行的第一个值是唯一标识符
            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实现的协程来做异步。通过ReadCSVFile接口来加载文件,加载完成后使用一个ReadCSVFinished 的回调函数获取加载好的数据。解析文件的细节在ReadTextToCSVTable函数中。
下面我们来看具体的使用:
在StreamAssets文件夹下准备一个测试用的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));
                }
            }
            //可以拿到表中任意一项数据
            Debug.Log(table["10011"]["id"]);
        });
    }

    void Update()
    {
        if (readFinish)
        {
            // 可以类似访问数据库一样访问配置表中的数据
            CSVLine line = CSVHelper.Instance().SelectFrom("csv_test").WhereIDEquals(10011);
            Debug.Log(line["name"]);
            readFinish = false;
        }
    }
}

运行游戏可以查看效果:


结语

本文介绍的方法实现了异步加载配置文件,并且可以像读取数据库一样读取数据。当然,要完全像使用SQL语句那样简便并且实现数据的各种组合比较困难,这里是夸张的说法。但对于读取配置信息已经足够。我希望读者可以实现自己的扩展,使读取数据更容易。如果有兴趣的话,也可以实现一个CSVWriter类用于数据写入。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容