常见的配表方式

  1. 基于Python的Excel与类的映射。最终生成一个python文件。
  2. 基于protobuf的Excel与类的映射。生成一个二进制。
  3. 程序生成xml,策划配置excel使用vb转成xml,运行时使用的是二进制。

教程中使用第三种的优化版,下图是基本流程。

配置表

在游戏运行时,程序内部基本都是类和二进制互相转化,二进制反序列化来读取配置,类序列化成二进制来存档等。

其余的转化操作都是编辑器环境下完成的。

配置表序列化封装

在之前整理了工程文件目录和程序集,截图如下:

文件目录

在ResFrame文件夹下新建ConfigFramework文件夹,并在其中新建BinarySerializeOpt脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Xml.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

public class BinarySerializeOpt
{
/// <summary>
/// 类序列化成xml
/// </summary>
/// <param name="path">xml生成位置</param>
/// <param name="obj">需要序列化的类</param>
/// <returns>是否成功</returns>
public static bool XmlSerialize(string path,System.Object obj)
{
try
{
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite))
{
using (StreamWriter sw = new StreamWriter(fs, System.Text.Encoding.UTF8))
{
//下面这两行用于取消xml最开头的声明,看需求使用
//XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();
//namespaces.Add(string.Empty, string.Empty);
XmlSerializer xs = new XmlSerializer(obj.GetType());
xs.Serialize(sw, obj);
}
}
return true;
}
catch (System.Exception e)
{

Debug.LogError("此类无法转换成xml"+ obj.GetType() + "," + e);
//根据需求,如果发生错误,可以把生成的文件进行删除
}
return false;
}
/// <summary>
/// 编辑器下直接使用xml文件转类
/// </summary>
/// <param name="path">xml文件路径</param>
public static T XmlDeserialize<T>(string path) where T : class
{
T t = default(T);
try
{
using (FileStream fs = new FileStream(path,FileMode.Open,FileAccess.ReadWrite,FileShare.ReadWrite))
{
XmlSerializer xs = new XmlSerializer(typeof(T));
t = xs.Deserialize(fs) as T;
}
}
catch (System.Exception e)
{

UnityEngine.Debug.LogError("此Xml无法反序列化成类:" + path + "," + e);
}
return t;
}
/// <summary>
/// 编辑器下使用xml文件转object
/// </summary>
/// <param name="path">xml文件路径</param>
public static object XmlDeserialize(string path,Type type)
{
object obj = null;
try
{
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
{
XmlSerializer xs = new XmlSerializer(type);
obj = xs.Deserialize(fs);
}
}
catch (Exception e)
{

UnityEngine.Debug.LogError("此Xml无法反序列化成object:" + path + "," + e);
}
return obj;
}

/// <summary>
/// 运行时使用转化成二进制的xml文件转类
/// </summary>
/// <param name="path">二进制文件路径</param>
public static T XmlDeserializeAtRuntime<T>(string path) where T : class
{
T t = default(T);
TextAsset textAsset = ResourceManager.Instance.LoadResource<TextAsset>(path);

if (textAsset == null)
{
UnityEngine.Debug.Log("Can't Load TextAsset : "+ path);
return null;
}

try
{
using(MemoryStream ms = new MemoryStream(textAsset.bytes))
{
XmlSerializer xs = new XmlSerializer(typeof(T));
t =xs.Deserialize(ms) as T;
}
ResourceManager.Instance.ReleaseResource(path);
}
catch (System.Exception e)
{

UnityEngine.Debug.LogError("Load TextAssetException: " + path + "," + e);
}
return t;
}
/// <summary>
/// 类转换成二进制
/// </summary>
/// <param name="path">二进制文件存储位置</param>
public static bool BinarySerialize(string path, System.Object obj)
{
try
{
using (FileStream fs = new FileStream(path,FileMode.Create,FileAccess.ReadWrite,FileShare.ReadWrite))
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, obj);
return true;
}
}
catch (System.Exception e)
{

UnityEngine.Debug.LogError("此类无法转化成二进制:" + obj.GetType() + "," + e.ToString());
}
return false;
}
/// <summary>
/// 读取二进制
/// </summary>
/// <param name="path">二进制文件路径</param>
public static T BinaryDeserialize<T>(string path) where T : class
{
T t = default(T);
TextAsset textAsset = ResourceManager.Instance.LoadResource<TextAsset>(path);

if (textAsset == null)
{
UnityEngine.Debug.Log("Can't Load TextAsset : " + path);
return null;
}

try
{
using (MemoryStream ms = new MemoryStream(textAsset.bytes))
{
BinaryFormatter bf = new BinaryFormatter();
t = bf.Deserialize(ms) as T;
}
ResourceManager.Instance.ReleaseResource(path);
}
catch (System.Exception e)
{

UnityEngine.Debug.LogError("Load TextAssetException: " + path + "," + e);
}
return t;
}
}

配置表管理

ExcelBase

在ResFrame——ConfigFramework文件夹内新建ExcelBase脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[System.Serializable]
public class ExcelBase
{
#if UNITY_EDITOR
/// <summary>
/// 在编辑器环境下进行反射调用,用于生成数据
/// </summary>
public virtual void Construction() { }
#endif
/// <summary>
/// 在运行时调用,将数据初始化出来
/// </summary>
public virtual void Init() { }
}

在GameData——Data文件夹下新建Xml文件夹和Binary文件夹

ConfigManager

ConfigManager

在ResFrame——ConfigFramework文件夹内新建ConfigManager脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConfigManager : SingletonPattern<ConfigManager>
{
/// <summary>
/// 储存所有已经加载的配置表
/// </summary>
protected Dictionary<string,ExcelBase> m_AllExcelData = new Dictionary<string,ExcelBase>();
/// <summary>
/// 加载配置表
/// </summary>
/// <param name="path">二进制配置表的路径</param>
public T LoadData<T>(string path) where T : ExcelBase
{
if (string.IsNullOrEmpty(path))
{
return null;
}

if (m_AllExcelData.ContainsKey(path))
{
Debug.LogError("重复加载相同配置文件" + path);
return m_AllExcelData[path] as T;
}

T data = BinarySerializeOpt.BinaryDeserialize<T>(path);

#if UNITY_EDITOR
if (data == null)
{
Debug.Log(path + " 不存在,从Xml加载数据。");
string xmlPath = path.Replace("Binary", "Xml").Replace(".bytes", ".xml");
data = BinarySerializeOpt.XmlDeserialize<T>(xmlPath);
}
#endif

if (data != null)
{
data.Init();
}
m_AllExcelData.Add(path, data);
return data;
}
/// <summary>
/// 根据路径查找配置数据
/// </summary>
/// <param name="path">配置的路径</param>
public T FindData<T>(string path) where T : ExcelBase
{
if(string.IsNullOrEmpty(path))
{
return null;
}
ExcelBase excelBase = null;
if (m_AllExcelData.TryGetValue(path, out excelBase))
{
return excelBase as T;
}
else
{
excelBase = LoadData<T>(path);
if(excelBase != null)
{
return excelBase as T;
}
}
return null;
}
}

public class CFGPaths
{
//各配置表的路径
public const string TABLE_MONSTER = "Assets/GameData/Data/Binary/MonsterData.bytes";
}

ConfigManager的调用

GameStart脚本中,添加LoadConfig方法并在Start最开始调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using UnityEngine;
using UnityEngine.EventSystems;

public class GameStart : MonoSingletonPattern<GameStart>
{
private GameObject m_GameObject;
public Transform recyclePoolTrans, sceneTrans;
public RectTransform uiRoot, windowRoot;
public Camera uiCamera;
public EventSystem eventSystem;
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(gameObject);
AssetBundleManager.Instance.LoadAssetBundleConfig();
ResourceManager.Instance.Init(this);//初始化异步加载
ObjectPoolManager.Instance.Init(recyclePoolTrans,sceneTrans);
}
private void Start()
{
LoadConfig();//它本身依赖于ResourceManager读取二进制文件,所以放到这里

UIManager.Instance.Init(uiRoot, windowRoot,uiCamera,eventSystem);
RegisterUI();
GameSceneManager.Instance.Init(this);

ResourceManager.Instance.LoadResource<AudioClip>(ConstentStr.MENUSOUND);

GameSceneManager.Instance.LoadScene(ConstentStr.MENUSCENE);

}
void RegisterUI()
{
UIManager.Instance.Register<MenuWnd>(ConstentStr.MENUPANEL);
UIManager.Instance.Register<LoadingWnd>(ConstentStr.LOADINGPANEL);
}
void LoadConfig()
{
ConfigManager.Instance.LoadData<MonsterData>(CFGPaths.TABLE_MONSTER);//+++
}

注意,在网游中热更之后,AB包会重新加载,所有的缓存都会清掉,配置文件也会重新加载,上面的调用方式只适合单机。

怪物类示例

怪物类

在Scripts文件夹下新建Excel文件夹,并在其中新建MonsterData文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System.Collections.Generic;
using System.Xml.Serialization;

[System.Serializable]
public class MonsterData : ExcelBase
{
/// <summary>
/// 编辑器下初始类转xml
/// </summary>
public override void Construction()
{
AllMonster = new List<MonsterBase>(5);
for (int i = 0; i < 5; i++)
{
MonsterBase monster = new MonsterBase
{
Id = i,
Name = i + "sq",
OutLook = "Assets/GameData/Prefabs/Attack",
Level = i,
Rarity = i,
Height = i + 1,
};
AllMonster.Add(monster);
}
}
/// <summary>
/// 数据初始化
/// </summary>
public override void Init()
{
m_AllMonsterDic.Clear();
foreach (MonsterBase monster in AllMonster)
{
if (m_AllMonsterDic.ContainsKey(monster.Id))
{
UnityEngine.Debug.LogError(monster.Name + " 有重复ID");
}
else
{
m_AllMonsterDic.Add(monster.Id, monster);
}
}
}
/// <summary>
/// 根据ID查找Monster数据
/// </summary>
public MonsterBase FindMonsterById(int id)
{
MonsterBase monster;
if (m_AllMonsterDic.TryGetValue(id, out monster))
{
return monster;
}
return null;
}
[XmlIgnore]
public Dictionary<int, MonsterBase> m_AllMonsterDic = new Dictionary<int, MonsterBase>();
[XmlElement("AllMonster")]
public List<MonsterBase> AllMonster { get; set; }
}
[System.Serializable]
public class MonsterBase
{
[XmlAttribute("Id")]
public int Id { get; set; }
[XmlAttribute("Name")]
public string Name { get; set; }
//预制体路径,存储预制体全路径,因为在运行时拼接字符串会产生很多GC
[XmlAttribute("OutLook")]
public string OutLook { get; set; }
[XmlAttribute("Level")]
public int Level { get; set; }
[XmlAttribute("Rarity")]
public int Rarity { get;set; }
[XmlAttribute("Height")]
public float Height { get; set; }
}

数据类需要继承ExcelBase,数据类内部维护各种配置,它有一个Dictionary用于快速查找各种数据。

数据的最小单位需要我们自己定制,这里使用MonsterBase来示例

Construction方法,用来在编辑器下反射调用,配合BinarySerializeOpt.XmlSerialize方法生成xml,这里面写的AllMonster = new List<MonsterBase>(5);是故意的,方便查看生成的xml格式,从而方便修改数据。

Init方法,是在游戏运行时调用,具体是在ConfigManager.LoadData<T>调用

注意,在游戏运行时,AllMonster这个列表是被二进制文件反序列化出来的,不要误会AllMonster列表只能在编辑器调用Construction方法的时候才被填充,编辑器填充这个列表是为了类转xml用的

类转xml和xml反序列化

在ResFrame.Editor——Editor文件夹内新建Config文件夹,并在其中新建DataEditor脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
using UnityEditor;
using System.Reflection;
using UnityEngine;
using System;

public class DataEditor
{
public static string XmlPath = "Assets/GameData/Data/Xml/";
public static string BinaryPath = "Assets/GameData/Data/Binary/";
public static string ScriptsPath = "Assets/Scripts/Data/";
public static string ExcelPath = Application.dataPath + "/../Data/Excel/";
public static string RegPath = Application.dataPath + "/../Data/Reg/";

[MenuItem("Assets/类转xml")]
public static void AssetsClassToXml()
{
UnityEngine.Object[] objs = Selection.objects;
for (int i = 0; i < objs.Length; i++)
{
EditorUtility.DisplayProgressBar("已经选中的类转成xml", "正在扫描" + objs[i].name + "……",1.0f/objs.Length * i);
ClassToXml(objs[i].name);
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();

}
private static void ClassToXml(string name)
{
if (string.IsNullOrEmpty(name))
return;

try
{
Type type = null;
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type curType = assembly.GetType(name);
if (curType != null)
{
type = curType;
break;
}
}
if(type != null)
{
var typeInit = Activator.CreateInstance(type);
if (typeInit is ExcelBase)
{
(typeInit as ExcelBase).Construction();
}
string xmlPath = XmlPath + name + ".xml";
BinarySerializeOpt.XmlSerialize(xmlPath, typeInit);
Debug.Log(name + "类转xml成功,xml路径为:" + xmlPath);
}

}
catch (Exception)
{

Debug.LogError(name + "类转xml失败");
}
}
private static object GetObjFromXml(string name)
{
if (string.IsNullOrEmpty(name))
return null;
try
{
Type type = null;
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies())
{
Type tempType = asm.GetType(name);
if (tempType != null)
{
type = tempType;
break;
}
}
if (type != null)
{
string xmlPath = XmlPath + name + ".xml";
return BinarySerializeOpt.XmlDeserialize(xmlPath, type);
}
return null;
}
catch
{
Debug.LogError(name + "xml反序列化失败");
return null;
}
}

}

我们使用反射查找我们选中的脚本名称对应的Type,这里我们通过AppDomain.CurrentDomain.GetAssemblies()来查找我们当前编辑器环境下所有的Assembly(包括自定义的),找到对应的第一个Type之后,生成此Type的实例并调用Construction()方法,再掉用BinarySerializeOpt.XmlSerialize生成xml文件

我们选中Scripts——ExcelData——MonsterData脚本,然后右键——“类转Xml”,这样,我们的GameData——Data——Xml文件夹内就会有生成的xml文件

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<MonsterData xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<AllMonster Id="0" Name="0sq" OutLook="Assets/GameData/Prefabs/Attack" Level="0" Rarity="0" Height="1" />
<AllMonster Id="1" Name="1sq" OutLook="Assets/GameData/Prefabs/Attack" Level="1" Rarity="1" Height="2" />
<AllMonster Id="2" Name="2sq" OutLook="Assets/GameData/Prefabs/Attack" Level="2" Rarity="2" Height="3" />
<AllMonster Id="3" Name="3sq" OutLook="Assets/GameData/Prefabs/Attack" Level="3" Rarity="3" Height="4" />
<AllMonster Id="4" Name="4sq" OutLook="Assets/GameData/Prefabs/Attack" Level="4" Rarity="4" Height="5" />
</MonsterData>

xml转二进制

DataEditor脚本中添加新的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[MenuItem("Assets/Xml转Binary")]
public static void AssetsXmlToBinary()
{
UnityEngine.Object[] objs = Selection.objects;
for (int i = 0; i < objs.Length; i++)
{
EditorUtility.DisplayProgressBar("已经选中的xml转成二进制", "正在扫描" + objs[i].name + "……", 1.0f / objs.Length * i);
XmlToBinary(objs[i].name);
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}
private static void XmlToBinary(string name)
{
//可以用MD5码来测试文件有没有修改,没有修改则不转化,这里省略
if (string.IsNullOrEmpty(name))
return;
try
{
Type type = null;
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type curType = assembly.GetType(name);
if (curType != null)
{
type = curType;
break;
}
}
if (type != null)
{
string xmlPath = XmlPath + name + ".xml";
string binaryPath = BinaryPath + name + ".bytes";
object obj = BinarySerializeOpt.XmlDeserialize(xmlPath,type);
BinarySerializeOpt.BinarySerialize(binaryPath, obj);
Debug.Log(name + "xml转二进制成功,二进制路径为:" + binaryPath);
}
}
catch (Exception)
{

Debug.LogError(name + "xml转二进制失败");
}
}

先将xml反序列化成类——再将类转化为二进制

我们选中GameData——Data——Xml内的MonsterData.xml,然后右键——“Xml转Binary”,这样,我们的GameData——Data——Binary文件夹内就会有生成的bytes文件

所有xml转二进制

DataEditor脚本中添加新的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[MenuItem("Tools/Xml/所有Xml转Binary")]
public static void AllXmlToBinary()
{
string xmlFullPath = Path.GetFullPath(XmlPath);
string[] filesPath = Directory.GetFiles(xmlFullPath, "*.*",SearchOption.AllDirectories);
for (int i = 0; i < filesPath.Length; i++)
{
EditorUtility.DisplayProgressBar("文件夹下所有的xml转二进制", filesPath[i], 1.0f / filesPath.Length * i);
if (filesPath[i].EndsWith(".xml"))//过滤掉.meta文件
{
string binaryName = Path.GetFileNameWithoutExtension(filesPath[i]);
XmlToBinary(binaryName);
}
}
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
}