數(shù)據(jù)驅(qū)動
數(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ù)寫入。