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

数据驱动

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

CSV文件

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

实践

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

我们首先来设计存储表中数据的类。

public class CSVLine : IEnumerable
{
	private Dictionary dataContainer = new Dictionary();

	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 item in dataContainer)
		{
			yield return item;
		}
	}
}

CSVLine类将用来存储表中每一行的数据。

public class CSVTable : IEnumerable
{
	private Dictionary dataContainer = new Dictionary();

	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();
		}
		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 readedTable = null;
	#endregion

	#region public interfaces
	public void ReadCSVFile(string fileName, ReadCSVFinished callback)
	{
		
		if (readedTable == null)
			readedTable = new Dictionary();
		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 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类用于数据写入。

稿源:Allen Kashiwa Blog (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 综合技术 » Unity开发中异步加载配置文件,像读取数据库一样读取配置信息

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录