常见的配表方式
基于Python的Excel与类的映射。最终生成一个python文件。
基于protobuf的Excel与类的映射。生成一个二进制。
程序生成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 { 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)) { XmlSerializer xs = new XmlSerializer(obj.GetType()); xs.Serialize(sw, obj); } } return true ; } catch (System.Exception e) { Debug.LogError("此类无法转换成xml" + obj.GetType() + "," + e); } return false ; } 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; } 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; } 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; } 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 ; } 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 public virtual void Construction () { } #endif public virtual void Init () { } }
在GameData——Data文件夹下新建Xml文件夹和Binary文件夹
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 > { protected Dictionary<string ,ExcelBase> m_AllExcelData = new Dictionary<string ,ExcelBase>(); 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; } 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(); 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 { 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); } } 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); } } } 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 ; } [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 ){ 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" )) { string binaryName = Path.GetFileNameWithoutExtension(filesPath[i]); XmlToBinary(binaryName); } } AssetDatabase.Refresh(); EditorUtility.ClearProgressBar(); }