撰写了文章 发布于 2018-09-19 15:34:17
Unity动态读取json文件数据并存储到类对象中进行调用
前言:
刚刚花一个星期的业余时间研究搞定了如何通过读取表格中每行的唯一ID(或者string注释)取到每一个数值的方法,作为新手(自认为是新手中的菜鸟)而言,一步一步的攻克每一个代码逻辑,也曾经一整天坐在电脑前不断尝试不断失败,足足花了一周的业余时间(加起来足有30个小时)才真正搞定,而且自觉依然不算很规范,并且有耗能过高的嫌疑。
而且要吐槽的是:搜遍百度也找不到一个成熟的读取文件的完整的解决方案,找插件也没有,果然是英文小白没人权啊 = = 慢慢补英文争取熟练运用Google吧。
不过作为第一篇代码日志而言,还是值得纪念的,下面详细写下思路以及会遇到的坑。
正文:
首先是分析下当前常用的从文件中读取数据的方法:
1.直接读取Excle的xls、xlsx中的数据,通过ExcleReader的dll进行引用,优点是可以跨平台,而且直接读策划的excle无疑在便利性上是最优方案。不过缺点是在IOS上不稳定,而且Excle的数据还要保存到另外的文件才可以进行游戏中的动态读取(这个方案我研究了20个小时,占了2/3的时间,示例代码使用时各种问题实在搞不懂只能作罢)。
C# Unity游戏开发--Excel中的数据是如何到游戏中的 (一)系列教程
2.使用常规Excle转Xml进行读取,xml是常用的数据格式文件,优点是扩展性强,可以支持多数文件的多个数据结构(例如千奇百怪的嵌套),缺点是读取速度慢。因为个人作为策划对于维护xml的经历是深恶痛绝,所以一开始就没打算使用xml。
3.使用Txt文本文件直接取值,优点是便捷易用,无须扩展插件,缺点很显然是不支持多数复杂的数据结构。
4.直接读取csv文件,然后取值到类对象中进行引用,优点跟Excle一样,缺点似乎也是无法动态调用。
5.各种文件(包括Excle、csv、xml)转json文件,json是一种轻量级的数据交换结构的文件,似乎在服务器与客户端交互上有很多优势,而且速度快,也非常易用。笔者最后找到一篇教程最终采用了这种方法。
本篇日志基本逻辑引用:Unity 保存Json数据到本地文件 和unity3d学习笔记(二十)--利用JSON读取和保存游戏数据 - lzhq1982的专栏 - 博客频道 - CSDN.NET的方法,但是教程中只写了个简单的setString取值,我做了下类对象的自动赋值与静态字典的存值,这样直接使用类对象就能在各种场合使用了。
提醒:本文的方法依然只适用于一列唯一ID确定一个值的办法,如果需要2-多个数据才能确定一个值,这个方案需要再进行类里面的更复杂的定义(如类对象定义字典或者list)。
实现方法:
1.首先是无耻的使用插件:ExcelToJson.unitypackage ,包含2个cs脚本文件:ExcelToJson和库文件:SimpleJson(这也是个堪比LitJson的库文件,里面功能只比LitJson少一点点)。只在Editor目录下进行运作,使用时点击Tools/ExcelToJson即可。我改了一下ExcelToJson.cs中的csv文件目录和输出目录,以及Editor模式中的按钮位置,更加方便使用。
修改Editor模式下的按钮位置以及修改目录(ExcelToJson.cs:代码内容见知乎专栏):
然后我嫌弃SimpleJson的应用范围有点窄,所以再次引入LitJson的dll文件,可以支持动态解析和生成文件,不要太好用。
2.接下来我们继续做准备工作:简单写一个csv表格(坑:csv文件里面使用的任何字段内容不支持任何非正常格式的字符串,不然会报错),理论上xls和xlsx再写点拓展代码也支持。
通过ExcelToJson转为Json文件(坑:这里转为的所有数据均为string类型,如果你想要实现更加复杂的数据结构的直接赋值,要么在类文件里面再重新赋值一次,要么找到一个更全面的ExcelToJson的工具脚本):
3.重头戏来了:这一段反反复复研究了四五个小时,最后才理清楚逻辑,果然文科生写代码就是一团浆糊的逻辑:
接下来进行2种取值方式的设计:
第一种:
使用每一层字典的key准确参数取值,优点是比较简单,缺点是如果代码里面充满了这种取值的办法,性能会有较大影响,而且不方便使用。
在脚本文件夹内(不要在Editor或者其他非打包内容文件夹内创建)创建cs文件:JsonReader.cs
引入新的命名空间:
using LitJson;
using System.Collections.Generic;
然后定义路径(文件夹和文件名)的私有变量和存储格式:
public static class JsonReader {
static string mFileName;
static string mFolderName;
//表格内第二级字段对应内容,表格第一列需要标明每一行的ID或者用途
public static Dictionary <string ,JsonData> data_Value = new Dictionary<string, JsonData> ();
public static Dictionary <string ,string> dic_Value = new Dictionary<string, string> ();
然后处理文件夹和文件名的传递
//文件路径,合并文件夹名和文件名
static string FileName{
get{ return Path.Combine (FolderName,mFileName);
}
}
//文件夹路径,合并persistentDataPath和文件夹名
static string FolderName{
get{ return Path.Combine ("/",mFolderName);
}
}
这里只处理了第一层嵌套的取值,data_Value拿到的Value只是另外一层jsondata!
使用Dictionary <string ,JsonData> 的格式进行初次取值。
(坑:不要使用IO的FileStream和Application.dataPath的绝对路径 ,因为打包后的绝对路径会找不到文件,建议使用Resources.Load)
//读取json内容
static void read(){
try {
//如果没有文件夹则创建之
if (!Directory.Exists (FolderName)) { Directory.CreateDirectory (FolderName);
Debug.Log ("没有找到json文件路径,创建文件夹:"+FolderName);
} TextAsset ta = Resources.Load(FileName) as TextAsset;
//保存json中的数据到jd
JsonData jd = JsonMapper.ToObject (ta.text);
foreach (var key in jd.Keys) { data_Value.Add(key,jd[key]);}
} catch (System.Exception ex) {
Debug.Log ("该键/值已经存在:"+ex);
}}
初始化文件路径和读取方法,用于重置数据,作为使用读取的前置方法。
/// 根据路径初始化Dictionary和文件路径信息读取文件
public static void init_Read(string pFolderName,string pFileName){ mFileName = pFileName; mFolderName = pFolderName; data_Value.Clear (); dic_Value.Clear (); read (); }
判断字典里面有没有包含输入的参数:
/// <summary>
/// 判断当前是否存在该key值
public static bool HasKey(string pKey) {
if (data_Value.ContainsKey (pKey)==false) {
Debug.Log ("【"+pKey+"】"+"查找失败!");
}
return data_Value.ContainsKey(pKey); } public static bool HasKey_Key(string pKey_Key) {
if (data_Value.ContainsKey (pKey_Key)==false) {
Debug.Log ("【"+pKey_Key+"】"+"查找失败!");
}
return dic_Value.ContainsKey(pKey_Key); }
然后再进行一次jsondata的循环,返回根据参数key去到的value。
/// 读取string值
public static string GetString(string pKey,string pKey_Key) { data_jd = data_Value [pKey];
foreach (var key in data_jd.Keys) { dic_Value.Add(key,data_jd[key].ToString());
}
//判断是否包含查找对象,否则返回空
if(HasKey(pKey) && HasKey_Key(pKey_Key)) { return dic_Value[pKey_Key]; } else { Debug.Log (pKey+" 或者 "+pKey_Key+" 查找失败!");
return string.Empty; } }
这一种取值的方法很简单,初始化后直接JsonReader.GetString(pKey,pKey_Key)即可。第一个key是第一层的key,第二个key_key是第二层的key,以此类推。
第二种:
使用JsonMapper.ToObject<>()直接向类对象里传值,一步到位!优点是代码实现简单,便于使用,推荐。缺点是表格太多静态保存的数据会在游戏初始化的时候加载很长时间。
在脚本文件夹内(不要在Editor或者其他非打包内容文件夹内创建)创建cs文件:JsonReader.cs;
using UnityEngine;
using LitJson;
using System.Collections.Generic;
public static class JsonReader
{
//读取json数据,根据id查找(表格里的第一行),返回id和类对象互相对应的字典。 public static Dictionary<string, T> ReadJson<T>(string fileName) { TextAsset ta = Resources.Load(fileName) as TextAsset; if (ta.text == null) { Debug.Log("根据路径未找到对应表格数据"); }; Dictionary<string, T> d = JsonMapper.ToObject<Dictionary<string, T>>(ta.text); return d; }
//写入json数据,传入类类型变量。 public static void WriteJson(string path, object jsonData) { JsonMapper.ToJson(jsonData); }
}
创建表格对应类Class:SD_Role和类对象Class:Class_Role;(坑:表格中的所有字段名要与Class:Class_Role内的字段名一样,数量和名字都要一摸一样!!!不然无法传值而且不会报错!!!这个任何LitJson的教程都没有说过,神坑之一!)
public static class SD_Role { //静态保存表格数据的字典,填写当前表格的文件路径,格式如:"Json/Document/Force",无须带后缀。 public static Dictionary<string, Class_Role> Role_Dic = JsonReader.ReadJson<Class_Role>("Json/Document/Force"); } public class Class_Role { public string name { get; set; } public string icon { get; set; } public string flag { get; set; } public string des { get; set; } public string color { get; set; } public string officeID { get; set; } }
注意:Class:Class_Role的所有对象必须为public而且不能是静态,不然无法修改数据。
使用很简单:
public UILabel role_name; void Start () { role_name.text = SD_Role.Role_Dic["1"].name; }
补充一下:如果要写入json文件,直接使用 JsonMapper.ToJson(jsonData)即可将类类型数据转为json字符串。
***********************************************************************
使用通用方法总结一下过程:
(1) 做了什么相关实例?
读取角色表格内的指定行和列的参数值,储存到类对象中。
(2)做得怎么样?
比较自动化,在扩展的时候直接使用相同的方式配不同的参数即可。
但是,这种做法将表格里面的所有值都作为string字符串,导致占用空间较多,而且不合理,无法由单机游戏向网络游戏扩展。
(3)有没有效率低下?
具体实现过程中尝试了很多效率低下的方法,而且本身研究消耗了一周时间,可见自身代码基础之差。
(4)如何提高效率?
提高代码知识,在过程中最严重的问题是不同代码的作用域区分不清楚,然后导致错误不知道消耗了多长时间。
(5)有没有更合适的解决方案?
全面实现自动化,应当对csv文件进行一次操作后自动生成类文件并向其中传值,这样只关心csv的表格内容即可,代码基本不用写。
(6)如果下次我会怎么做?
作为解决方案来讲,马马虎虎,还有很大优化的空间。下次会持续进行自动化优化。
上个月基本啥都没干,基本都在折腾设计案和文件处理,咨询了一位大神,总算彻底梳理通了,今天把Editor模式下的转换模式补充了一下,算是CSV文件读取工作彻底完成。
读取json的【jsonReader】见上一篇日志:知乎专栏
这次把写的比较粗糙的生成json和CS文件的代码贴上来。【ExcelToJson.cs】
首先是命名空间,需要UnityEditor和SimpleJSON,因为是直接在工具的基础上改的,所以就没有改成LitJson,反正不是游戏代码,只是文件处理工具,多一个SimpleJSON文件也不会打包进去。
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using System.Text;
using SimpleJSON;
接下来是2个导出文件的方法,本来只应用于json文件的导出。
在这个方法里面,MenuItem的作用是在Editor模式下在untiy中添加菜单选项ExcelToJson按钮,点击则使用此脚本。注意:使用MenuItem等添加editor菜单的方法一定要是static修饰的方法,否则没有按钮出现的。
这个方法的主要作用是调用outJsonContentToFile导出json文件,顺便设定下csv表格和json导出的路径。
[MenuItem("Tools/ExcelToJson")]
static void excelToJson()
{
string dataFolderPath=Application.dataPath+ "/Document";
string outJsonPath=Application.dataPath+ "/Resources/Json";
if(!Directory.Exists(dataFolderPath))
{
Debug.LogError("请建立"+dataFolderPath+ " 文件夹,并且把csv文件放入此文件夹内");
return;
}
string[] allCSVFiles=Directory.GetFiles(dataFolderPath,"*.csv");
if(allCSVFiles==null||allCSVFiles.Length<=0)
{
Debug.LogError(""+dataFolderPath+ " 文件夹没有csv文件,请放入csv文件到此文件夹内");
return;
}
if(!Directory.Exists(outJsonPath))
{
Directory.CreateDirectory(outJsonPath);
}
for(int i=0;i<allCSVFiles.Length;i++)
{
string dictName=new DirectoryInfo(Path.GetDirectoryName(allCSVFiles[i])).Name;
string fileName=Path.GetFileNameWithoutExtension(allCSVFiles[i]);
string jsonData=readExcelData(allCSVFiles[i]);
outJsonContentToFile(jsonData,outJsonPath+"/"+dictName+"/"+fileName+".json");
}
}
然后就是接收json文本数据实现导出的方法,注意:需要添加UNITY_EDITOR的逻辑,目的是使用代码刷新unity的文件目录,不然你需要手动刷新,不刷新json文件是不会生成的。
static void outJsonContentToFile(string jsonData,string jsonFilePath)
{
string directName=Path.GetDirectoryName(jsonFilePath);
if(!Directory.Exists(directName))
{
Directory.CreateDirectory(directName);
}
File.WriteAllText(jsonFilePath,jsonData,Encoding.UTF8);
Debug.Log("成功输出Json文件 :"+jsonFilePath); //在Editor模式下重新导入文件数据,刷新。
#if UNITY_EDITOR AssetDatabase.Refresh();
#endif }
接下来就是处理csv数据和自动生成代码文件的主要逻辑了。详细讲解在下面进行——
static string readExcelData(string fileName) { if (!File.Exists(fileName)) { return null; } string fileContent = File.ReadAllText(fileName, UnicodeEncoding.Default); string[] fileLineContent = fileContent.Split(new string[] { "\r\n" }, System.StringSplitOptions.None); string class_name = Path.GetFileNameWithoutExtension(fileName); if (fileLineContent != null) { //注释的名字 string[] noteContents = fileLineContent[0].Split(new string[] { "," }, System.StringSplitOptions.None); //变量的名字 string[] VariableNameContents = fileLineContent[1].Split(new string[] { "," }, System.StringSplitOptions.None); //变量类型的名字 string[] TypeValue = fileLineContent[2].Split(new string[] { "," }, System.StringSplitOptions.None);
/*———————————生成CS的Class类脚本————————————*/
StringBuilder code = new StringBuilder(); //创建代码串 //添加常见且必须的引用字符串 code.Append("using UnityEngine; \n"); code.Append("using System.Collections; \n"); //产生类,所有可执行代码均在此类中运行 code.Append("public class Class_" + class_name + " { \n\t"); for (int i = 0; i < TypeValue.Length; i++) { code.Append("public string "); code.Append(VariableNameContents[i] + " { get; set; } "+ " //" + noteContents[i] + "\n\t"); if (TypeValue[i] == "int") { code.Append("public int _" + VariableNameContents[i] + " (){\n\t\t"); code.Append("int value = int.Parse(" + VariableNameContents[i] + ");\n\t\t"); code.Append("return value;\n\t"); code.Append("}\n\t"); } else if (TypeValue[i] == "float") { code.Append(" public float _" + VariableNameContents[i] + " (){\n\t\t"); code.Append("float value = float.Parse(" + VariableNameContents[i] + ");\n\t\t"); code.Append("return value;\n\t"); code.Append("}\n\t"); } else if (TypeValue[i] == "string") { code.Append(" public string _" + VariableNameContents[i] + " (){\n\t\t"); code.Append("string value = " + VariableNameContents[i] + ";\n\t\t"); code.Append("return value;\n\t"); code.Append("}\n\t"); } } code.Append("}\n\t"); string CSharpFilePath = Application.dataPath + "/Scripts/Script_Doc_CD"; string directName = Path.GetDirectoryName(CSharpFilePath); if (!Directory.Exists(directName)) { Directory.CreateDirectory(directName); } if (!Directory.Exists(CSharpFilePath)) { Directory.CreateDirectory(CSharpFilePath); }
FileStream fs = new FileStream(CSharpFilePath + "/" + "/Class_" + class_name + ".cs", FileMode.OpenOrCreate, FileAccess.Write); StreamWriter sw = new StreamWriter(fs, Encoding.UTF8); sw.Write(code.ToString()); sw.Close(); fs.Close(); //File.WriteAllText(CSharpFilePath + "/" + CDicdictName + "/Class_" + CDicfileName + ".cs", code.ToString(), Encoding.UTF8); Debug.Log("成功生成c#的Class文件" + class_name + ".cs" + "在目录:" + CSharpFilePath + " 中");
/*———————————生成CS的Dictionary类脚本————————————*/
StringBuilder code_Dic = new StringBuilder(); //创建代码串 string docPath = "\"Json/Document/" + class_name + "\""; //添加常见且必须的引用字符串 code_Dic.Append("using UnityEngine; \n"); code_Dic.Append("using System.Collections.Generic; \n"); //产生类,所有可执行代码均在此类中运行 code_Dic.Append("public static class SD_" + class_name + " { \n\t"); code_Dic.Append("public static Dictionary<string, Class_" + class_name + "> Class_Dic = JsonReader.ReadJson<Class_" + class_name + "> (" + docPath + ");\n\t"); code_Dic.Append("}\n"); string DicFilePath = Application.dataPath + "/Scripts/Script_Doc_SD"; string DicName = Path.GetDirectoryName(DicFilePath); if (!Directory.Exists(DicName)) { Directory.CreateDirectory(DicName); } if (!Directory.Exists(DicFilePath)) { Directory.CreateDirectory(DicFilePath); }
FileStream fs2 = new FileStream(DicFilePath + "/" + "/SD_" + class_name + ".cs", FileMode.OpenOrCreate, FileAccess.Write); StreamWriter sw2 = new StreamWriter(fs2, Encoding.UTF8); sw2.Write(code_Dic.ToString()); sw2.Close(); fs2.Close(); //File.WriteAllText(DicFilePath + "/" + DDicdictName + "/SD_" + DDicfileName + ".cs", code_Dic.ToString(), Encoding.UTF8); Debug.Log("成功生成c#的Dic文件" + class_name + ".cs" + "在目录:" + DicFilePath + " 中");
/*————————解析表格字符串————————————*/ JSONClass jsonData = new JSONClass(); for (int i = 3; i < fileLineContent.Length - 1; i++) { string[] lineContents = fileLineContent[i].Split(new string[] { "," }, System.StringSplitOptions.None); JSONClass classLine = new JSONClass(); for (int j = 1; j < lineContents.Length; j++) { classLine[VariableNameContents[j]] = lineContents[j]; } jsonData[lineContents[0]] = classLine; }
string resultJson = jsonData.ToString(""); return resultJson; } return null; }
这一段代码比较复杂,中间使用了3条分割线用于说明代码块的区域。
第一段是使用File.ReadAllText读取csv里面的数据保存为string类型的数组,然后处理文件名的路径,得到纯粹的文件名,再得到注释的名字数组、变量的名字数组、变量类型的数组。
在这里的代码逻辑中,约定csv表格内容的填写方式如下:
1.第一行为注释(中文)行,不作处理。
2.第二行为字段名行,非数据。
3.第三行填写字段的类型,用于生成cs文件。
4.第一列为每一行的唯一索引id,用于找到每一行使用。
如果不按照上述填法就会发生报错或者取不到数值的情况。
第二段为生成cs的Class类的脚本。使用StringBuilder和code.Append()存储我们写入的每一行的代码的文本,使用\n换行,使用\t缩进。Class类的数据我们通过上面的取值可以得到。
注意:在这里只进行了int、float和string的方法封装,如需其他数据类型自行添加,同时请务必保证csv填写不会出现无法转换的字符。
第三段为生成cs的字典的类的脚本,用于存放静态的字典数据,通过jsonReader得到每个表格中的所有数据存储到字典中,再赋给每个类对象中的每个字段数据。由于是静态存储的字典,所以修改数据时需要在相关代码中再进行创建字典存储修改后的数据。
注意:我在这里使用了StreamWriter而非file.writeAllText是因为后者会发生没有权限写入数据的情况(很诡异,因为在第一个方法里面用了writeAllText却没有问题)。
第四段为解析表格字符串的代码,逻辑也很简单,通过存储每一行的数据按照json的格式返回总的字符串,然后通过前两个方法写入json文件。
生成后的代码文件如下:
取值的方式在jsonReader的逻辑里面说过了就不再赘述。
目录
汪汪仙贝 1年前
无文 [作者] 1年前
发布
Artyficial 1年前
无文 [作者] 1年前
Artyficial 1年前
无文 [作者] 1年前
发布
汪汪仙贝 1年前
前段时间项目从Unity的插件商店里下了一个插件叫做Json .Net for Unity,能满足的json转换需求了(C#对象转换成json字符串,json字符串转换成C#对象),除了不能动态匹配对象的子类以外感觉没什么毛病了,C#中的List/Array和json数组相互转换也基本正确(对象存在初始化方法时可能存在冲突),注意点使用还是可以的。
发布