C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一)

引言

现在做游戏开发的没有几个不用Excel的,用的最多的就是策划。尤其是数值策划,Excel为用户提供强大的工具,各种快捷键,各种插件,各种函数。但是作为程序来说其实关注的不是Excel而是它最终形成的数据,而在程序中数据其实就是二进制,比如说一个int型就是4个byte,一个字母占2个byte。但是游戏中不可能把excel文件放进去(因为Excel本身就会占一部分额外的空间),也不可能把处理Excel的类库打包到程序,所以现在大多是对Excel进行读取然后将数据进行序列化并写入文件再打包,程序运行的时候直接读取数据文件在进行反序列化。

这样做有几个好处:1.节省了空间。2.把和游戏无关的操作放在了游戏之外。3.游戏开发过程中用起来也比较方便,因为反序列换后的数据就是自己定义的某个类型的一个变量。3.减少了前后端通讯传输的数据量。4.方便对数据进行加密。

其实这个过程的核心部分都是一次性的操作,只要实现一遍以后就可以重复使用。

 

流程

实现的流程大致可以分为三个部分:1.读取Excel文件。2.将读取的数据进行序列化并写入文件。3.反序列化。#4.加密

 

读取Excel

本来读取Excel是一件非常简单的事情,但是开始做才发现,对于C#来说Excel操作的类库很多,所以用哪个类库就成了一个问题,其实这些操作都是在游戏之外进行的类库的性能可以忽略,但是一般人肯定都喜欢用性能好一点的。

另外一个问题就相对比较重要了,很多Excel类库都是基于windows开发环境的,而且还需要安装office,换到其他平台就不支持,而且Unity是换平台游戏引擎,所以基于这几点,我最后选择了一个跨平台操作Excel的开源类库​ExcelReader

 

using UnityEngine;
using UnityEditor;
using System.Collections;
using System;
using System.Collections.Generic;
using System.IO;
using Excel;
using System.Data;

public class ExportExcel {

[MenuItem("Frame/ExportExcel")]
public static void ExportExcelToBinary()
{

Dictionary<string, List<List<Property>>> DataMap = new Dictionary<string, List<List<Property>>>();
List<List<Property>> classes;
List<Property> properties;

FileInfo info;
FileStream stream;
IExcelDataReader excelReader;
DataSet result;

string[] files = Directory.GetFiles(Application.dataPath + "/EasyUI/ExcelFiles", "*.xlsx", SearchOption.TopDirectoryOnly);

int row = 0, col = 0;

try
{
foreach (string path in files)
{
info = new FileInfo(path);
stream = info.Open(FileMode.Open, FileAccess.Read);
excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream);
result = excelReader.AsDataSet();

classes = new List<List<Property>>();

int rowCount = result.Tables[0].Rows.Count;
int colCount = result.Tables[0].Columns.Count;

for (row = 1; row < rowCount; row++)
{
properties = new List<Property>();

for (col = 0; col < colCount; col++)
{
//string name = result.Tables[0].Rows[0][j].ToString();
//string value = result.Tables[0].Rows[i][j].ToString();
//string type = result.Tables[1].Rows[1][col].ToString();
//Debug.Log(result.Tables[0].Rows[0][col].ToString()+":"+result.Tables[0].Rows[row][col].ToString());
properties.Add(
new Property(
result.Tables[0].Rows[0][col].ToString(),
result.Tables[0].Rows[row][col].ToString(),
result.Tables[1].Rows[1][col].ToString()
));
Debug.Log(result.Tables[1].Rows[1][col].ToString());
}

classes.Add(properties);
}

DataMap.Add(info.Name, classes);
excelReader.Close();
stream.Close();
}
}
catch(IndexOutOfRangeException exp)
{
Debug.LogError("数组下标超出范围!");
}

}
}

先把数据读到这里面Dictionary<string, List<List<Property>>> DataMap;后面会讲怎么利用meta data来做映射,怎么序列化。

 

还有就是Excel文件是有格式要求的哦,文件名就是类名,第一个sheet是数据从第二行开始读。第二个sheet的第一行是字段名称,第二行是字段类型,第三行的第一列是预留的类名。如下图

Unity游戏开发——Excel数据读取到游戏中_System

Unity游戏开发——Excel数据读取到游戏中_System_02

Unity游戏开发——Excel数据读取到游戏中_List_03

还有一个Property类是为了以后映射和序列化使用的

Unity游戏开发——Excel数据读取到游戏中_数据_04

using System;public class Property {

public string name;

public string value;

public string type;
public Property(string name,string value,string type)
{
this.name = name;
this.value = value;
this.type = type;
}
public Type GetType()
{
return null;
//todo..
}
}

 

看了很多帖子,比较有用的先给大家贴出来。http://www.mamicode.com/info-detail-494944.html , http://www.xuanyusong.com/archives/2429/

,加密:http://wenku.baidu.com/view/031ede00964bcf84b9d57b6e.html


C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)

本帖是延续的:​​C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一)​​

 

上个帖子主要是讲了如何读取Excel,本帖主要是讲述读取的Excel数据是如何序列化成二进制的,考虑到现在在手游中应用很广泛的序列化技术Google的ProtoBuf,所以本文也是按照ProtoBuf的方式来操作的。ProtoBuf是一个开源库,简单来说ProtoBuf就是一个能使序列化的数据变得更小的类库,当然这里指的更小是相对的。好了ProtBuf的东西就不在多说,以后会专门写一篇帖子的。本帖其实就相当于上帖中列出的“流程”中的2.将读取的数据进行序列化并写入文件。虽然这句话只有16个字,但是他的内容绝对不止16个字这么简单。

 

一开始计划的是这样的:1.先把Excel数据读出来。2.然后根据数据生成相应的类代码。3.然后再生成代码的实例并且通过反射(Reflection)把Excel中的数据填进去。4.最后用ProtBuf进行序列化。这听起来似乎很合理,不过在实现过程中还是遇到了几个棘手的问题。但是最重要的一个问题是,3.生成代码的实例并且通过反射(Reflection)把Excel中的数据填进去,中“生成实例”的过程。

 

在运行时生成的代码知识一串字符而已,想要生成字符串中的类的实例需要用到动态编译把代码编译后生成一个Assembly,然后用Assembly中的CreateInstance方法创建一个object类型的实例,这个实例就是字符串代码中的类的实例。但是,我们使用了ProtoBuf数据的格式来生成代码,所以工程中直接把ProtBuf的源码放进来而不是引入protobuf-net.dll,所以在动态编译设置编译参数来动态引入DLL文件的时候经常出错而导致,编译生成的Assembly是null。所以考虑到这些本文使用了一种折中的方案来跳过动态编译这个过程。

因为在Unity项目中我们关注的只是最终生成的二进制文件本身所以对于它是如何生成的对于Unity项目来说是没有任何影响的,只要保证数据的格式和生成代码的格式相对应一切就OK。基于此,我们把这个过程分成了两部分,1.生成代码。2.生成二进制文件(其实就是把编译的工作交给了编译器。。。)OK,话不多说了,帖代码吧!

 

1.生成代码

public static void GenerateCode()    {
FileInfo info;
FileStream stream;
IExcelDataReader excelReader;
DataSet result;
string[] files = Directory.GetFiles(Application.dataPath + "/EasyUI/ExcelFiles", "*.xlsx", SearchOption.TopDirectoryOnly);

string staticDataClassCode = "";

try
{
int priority1 = 1;
string code;
foreach (string path in files)
{
info = new FileInfo(path);
stream = info.Open(FileMode.Open, FileAccess.Read);
excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream);
result = excelReader.AsDataSet();
int rowCount = result.Tables[0].Rows.Count;
int colCount = result.Tables[0].Columns.Count;
string className = result.Tables[1].Rows[2][0].ToString();

staticDataClassCode += " [ProtoMember(" + priority1++ + ")]\n";
staticDataClassCode += " public List<" + className + "> " + className + "List = new List<" + className + ">();\n";

code = "";
code += "using System.Collections;\n";
code += "using System.Collections.Generic;\n";
code += "using ProtoBuf;\n";
code += "[ProtoContract]\n";
code += "public class " + className + "\n";
code += "{\n";
int priority2 = 1;
for (int col = 0; col < colCount; col++)
{
code += " [ProtoMember(" + priority2++ + ")]\n";
code += " public " + result.Tables[1].Rows[1][col].ToString() + " " + result.Tables[1].Rows[0][col].ToString() + ";\n";
}
code += " public " + className + "()\n";
code += " {}\n";
code += "}\n";
WriteClass(Application.dataPath + "/Script/Datas/" + className + ".cs", className, code);

excelReader.Close();
stream.Close();
}
code = "";
code += "using System.Collections;\n";
code += "using System.Collections.Generic;\n";
code += "using ProtoBuf;\n";
code += "[ProtoContract]\n";
code += "public class StaticData\n";
code += "{\n";
code += staticDataClassCode;
code += " public StaticData(){}\n";
code += "}\n";
WriteClass(Application.dataPath + "/Script/Datas/StaticData.cs", "StaticData", code);
}
catch (IndexOutOfRangeException exp)
{
Debug.LogError(exp.StackTrace);
}
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
}


 

2..生成二进制文件

public static void GenerateBinFile()    {
FileInfo info;
FileStream stream;
IExcelDataReader excelReader;
DataSet result;
string[] files = Directory.GetFiles(Application.dataPath + "/EasyUI/ExcelFiles", "*.xlsx", SearchOption.TopDirectoryOnly);
int row = 0, col = 0;
string name = "", value = "", type = "";
StaticData staticData = new StaticData();

foreach (string path in files)
{
info = new FileInfo(path);
stream = info.Open(FileMode.Open, FileAccess.Read);
excelReader = ExcelReaderFactory.CreateOpenXmlReader(stream);
result = excelReader.AsDataSet();
int rowCount = result.Tables[0].Rows.Count;
int colCount = result.Tables[0].Columns.Count;

string className = result.Tables[1].Rows[2][0].ToString();
FieldInfo field = staticData.GetType().GetField(className + "List");//获取类中的一个Field
object fieldValue = field.GetValue(staticData);//给这个实例中的Field的代表的属性赋值
IList list = fieldValue as IList;

for (row = 1; row < rowCount; row++)
{
Type tt = DataManager.Instance.GetType(className);
object obj = System.Activator.CreateInstance(tt);

for (col = 0; col < colCount; col++)
{
name = result.Tables[1].Rows[0][col].ToString();
value = result.Tables[0].Rows[row][col].ToString();
type = result.Tables[1].Rows[1][col].ToString();

Debug.Log(className);
FieldInfo ifo = tt.GetField(name);
object cvalue = System.ComponentModel.TypeDescriptor.GetConverter(ifo.FieldType).ConvertFrom(value);
ifo.SetValue(obj, cvalue);
}
if (list != null)
list.Add(obj);
}
excelReader.Close();
stream.Close();
}

using (FileStream fstream = File.Create(Application.dataPath + "/StaticDatas.bin"))
{
Serializer.Serialize(fstream, staticData);
}
AssetDatabase.Refresh();
AssetDatabase.SaveAssets();
}

说明一下DataManager.Instance.GetType(className);

Type.GetType(string)会默认从当前程序及搜索类型,而我的类文件是放到了Unity的Editor文件下,Unity会把这个文件识别为属于Editor程序集,但是生成的代码是放到了其他文件夹下Unity会把文件识别为另一个程序集,这个DataManager一定要放到和生成的类文件的相同程序集中。代码如下

Unity游戏开发——Excel数据读取到游戏中_数据_04

using UnityEngine;using System.Collections;
using System;

public class DataManager {

private static DataManager _instance;
public static DataManager Instance
{
get
{
if (_instance == null)
_instance = new DataManager();
return _instance;
}
}
public DataManager()
{
_instance = this;
}
public Type GetType(string name)
{
return Type.GetType(name);
}
}


 

将代码写入文件的方法


private static void WriteClass(string path,string className,string code)    {
System.IO.File.WriteAllText(path, code, System.Text.UnicodeEncoding.UTF8);
}


 

把动态编译代码的方法也贴出来吧,方便以后用到

private static Assembly compileCode(string fullClassName, string code)    {
//创建编译器
CSharpCodeProvider provider = new CSharpCodeProvider();
//设置编译参数
CompilerParameters paras = new CompilerParameters();
//paras.ReferencedAssemblies.Add("System.Collections.dll");
//paras.ReferencedAssemblies.Add("System.Collections.Generic.dll");
paras.ReferencedAssemblies.Add("System.dll");
paras.ReferencedAssemblies.Add("protobuf-net.dll");
//paras.OutputAssembly = "ScriptData.dll";
//编译成exe还是dll
paras.GenerateExecutable = false;
//是否写入内存,不写入内存就写入磁盘
paras.GenerateInMemory = true;
CompilerResults result = provider.CompileAssemblyFromSource(paras, code);//编译代码
Assembly as1 = result.CompiledAssembly; //获得编译后的程序集
return as1;
}

 

OK,文件已经生成了,经测试两个10K的Excel文件打包成一个StaticData.bin文件,1K!!!

不过一般游戏中的数据表无论是内容还是数量都远远大于这个数量。所以一般生成的bin文件还要进行压缩,或者用Unity打包,哦了,就先到这把剩下的内容下一帖继续。

 


http://stackoverflow.com/questions/2522376/how-to-choose-between-protobuf-csharp-port-and-protobuf-net

 


C# Unity游戏开发——Excel中的数据是如何到游戏中的 (三)

本帖是延续的:​​C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)​​

 

前几天有点事情所以没有继续更新,今天我们接着说。上个帖子中我们看到已经把Excel数据生成了.bin的文件,不过其实这样到游戏中还是不能用的。主要有两个方面,1.bin文件的后缀使我们随便取的名字,但是这种文件Unity不买账。因为Unity中的二进制文件必须是以.bytes命名的。2.在写文件之前其实还可以对二进制进行压缩,这样可以最大化节省设备空间。也就是说我们在生成数据实例后还需要做以下几件事:序列化 -> 压缩 -> 写文件.

 

 

方式和上帖中的方式基本相同,但是因为我们要对序列化的数据进行压缩,所以不能直接把数据序列化进文件流,二是序列化进一个MemoryStream然后取出二进制数据压缩后再进行写文件。

MemoryStream ms = new MemoryStream();        Serializer.Serialize<StaticData>(ms, (StaticData)staticData);
byte[] byts = ms.ToArray();
System.IO.FileStream stream1 = new System.IO.FileStream(Application.dataPath + "/Resources/StaticDatas.bytes", System.IO.FileMode.Create);
System.IO.BinaryWriter writer = new System.IO.BinaryWriter(stream1);
writer.Write(GZIP.Encode(byts));
writer.Close();
stream1.Close();


哦啦,加上这几句就Ok了。说明一下我们使用了ICSharpCode.SharpZipLib.GZip这个dll,GZIP的使用方法可以参考这个地址 http://community.sharpdevelop.net/forums/t/11005.aspx,这里我写了个GZIP工具类就两个方法

public static byte[] Encode(byte[] bin)    {
MemoryStream ms = new MemoryStream();
GZipOutputStream gzp = new GZipOutputStream(ms);
gzp.Write(bin, 0, bin.Length);
gzp.Close();
return ms.ToArray();
}

public static byte[] Decode(byte[] bin)
{
GZipInputStream gzp = new GZipInputStream(new MemoryStream(bin));
MemoryStream re = new MemoryStream();
int count = 0;
byte[] data = new byte[4096];
while ((count = gzp.Read(data, 0, data.Length)) != 0)
{
re.Write(data, 0, count);
}
return re.ToArray();
}

然后,我们在看看游戏中是怎么用的。使用也很简单,因为前面已经把类什么的都生成好了,所以,看代码把。

byte[] bytes = (Resources.Load("StaticDatas") as TextAsset).bytes;        MemoryStream ms = new MemoryStream(GZIP.Decode(bytes));
StaticData data = Serializer.Deserialize<StaticData>(ms);


现在我们游戏中就能看到数据了,使用的时候直接data.就可以啦。

哦了,至此本课题结束。

每个人都会经历从不会到会的过程,如果有什么纰漏还请大家多指教!谢谢啦!