easyui表格编辑事件_Unity手游开发札记——从Odin插件聊基于元数据的编辑器实现
最近一個多月的時間在全力做新項目的Demo,由于程序暫時還只有我一個人,所以從程序架構(gòu)搭建到戰(zhàn)斗邏輯實現(xiàn),再到編輯器開發(fā)都是我自己的工作。之前有較長一段時間日常的工作內(nèi)容已經(jīng)集中在團隊管理、圖形渲染、性能優(yōu)化等方面,在具體業(yè)務中做的內(nèi)容比較少了,這段時間重拾游戲玩法的開發(fā),雖然除了進度壓力之外沒有太多技術(shù)挑戰(zhàn),但也借這個機會回顧和反思之前的一些做法和代碼,有不少收獲。另外一個感受就是——如果不想那么多,埋頭醉心于玩法的實現(xiàn),每天寫上大幾百行代碼,也可以獲得最為簡單而直接的成就感。
當然,代碼量并不是最為直接的成就感來源,如果可以用更少的代碼實現(xiàn)更強大的功能,才是作為最會“偷懶”的程序員最為理想的工作方式。也恰恰是這個原因,才催生各種應對變化的設計模式,有了github這樣的開源社區(qū),以及開頭提到的元編程(Meta Programming)這樣的編程理念。
我們暫時放下對于編程理念的討論,先從需求的根源來看一下它可能的一個應用環(huán)境——編輯器的開發(fā)。
1. 編輯器 vs Excel
當你需要策劃編輯一份數(shù)據(jù)的時候,你問他——你是想用Excel填寫還是需要我?guī)湍阕鲆粋€編輯器?不同的人可能有不同的回答,這取決于策劃的經(jīng)驗、喜好,也會受到數(shù)據(jù)結(jié)構(gòu)的復雜程度的影響。爭論這兩者的優(yōu)劣沒有什么意義,找到他們各自的適用場景才是關(guān)鍵所在。
Excel本身具有超級強大的功能,它的設計目的就是編輯二維數(shù)據(jù),并在此之上構(gòu)建了豐富的統(tǒng)計和分析功能,如果再加上vba的幫助,那簡直無所不能。之前就認識一個策劃,整個游戲的數(shù)值演算以及帶有交互的基本Demo原型都在Excel中直接進行了實現(xiàn)。當然,它的缺點也比較明顯,就像MySQL之于Mongo一樣,Excel對于非結(jié)構(gòu)化的數(shù)據(jù)支持比較麻煩,比如技能這樣相對復雜的數(shù)據(jù)就要拆表等方式來描述,策劃填寫時需要跨越多張表格,不夠直觀,比較費時,也容易出錯。
編輯器在應對非結(jié)構(gòu)化數(shù)據(jù)時就相對容易一些,界面和操作方式可以根據(jù)需求直接定制,和游戲內(nèi)具體對象的交互也比較方便,比如技能編輯器中常常需要的預覽動作、特效等功能,編輯器更容易做到所見即所得,對于一些資源的填寫也可以直接使用選擇的方式,而不需要手動復制和修改,更加不容易出錯。
在新的項目中,游戲的戰(zhàn)斗邏輯選擇放置在了Lua層,因此和戰(zhàn)斗緊密結(jié)合的技能數(shù)據(jù)也只能選擇放置在Lua端。最初的戰(zhàn)斗Demo只設置了Lua Table的數(shù)據(jù)格式,然后通過直接編寫這個Lua文件就可以更改技能的各種效果。最終讓策劃來進行技能編輯工作的時候一開始也想要不嘗試下Excel的方式,但最后還是選擇了為策劃編寫一個簡單的技能編輯器,主要原因有這么幾點:
2. 基于元數(shù)據(jù)的編輯器框架
記憶中大約是在大三的時候,給我們上軟件工程課程的老師在課堂上說他帶的學生在編寫可以寫代碼的代碼,一臉神秘和驕傲。具體描述的應用場景已然流逝在了時間的長河中,但這所帶給在當時還只會C語言和C++以及一些基本編程知識的我的那種驚訝和震撼卻留在了我的記憶中。再到后來,雖然接觸了http://ASP.net、還有WPF這些框架,但對于那些由xml或者別的格式來描述的界面信息最終是如何轉(zhuǎn)變?yōu)橐粋€可以響應邏輯事件的控件的,并沒有非常清楚的認知,只是停留在使用的層面。直到后來做《無盡戰(zhàn)區(qū)》項目,最初大量的編輯器都是使用引擎內(nèi)部的UI來進行開發(fā)的,也就是和游戲UI是同一套東西,編寫起來比較麻煩,老大說我們要做一套可以根據(jù)配置自動生成界面的元數(shù)據(jù)框架,這樣可以大大提高比如技能編輯器這種需要大量界面工作的開發(fā)和迭代效率。
這是我第一次深入的去思考和理解通過配置來描述一個界面的方法,以及如何最終將配置轉(zhuǎn)變?yōu)橐粋€個的界面元素,從而組合成一個交互界面。以技能編輯器為例,整個框架所要包含的核心模塊可以用下圖這樣一個結(jié)構(gòu)來大致描述:
首先要有一些基礎(chǔ)的界面通用控件,Button、Text、Slider等等,然后要提供自動化的界面布局功能。編寫編輯器的程序需要編寫的只是一份描述某個數(shù)據(jù)自身特性的元數(shù)據(jù)文件,在這里也就是描述技能的元數(shù)據(jù)信息,比如描述了技能一個技能包含哪些字段,這些字段分別是什么類型,按照什么樣的方式讓策劃編輯,取值范圍是多少等等。這些信息按照框架定義好的方式進行描述,元數(shù)據(jù)解析功能就可以解析這些配置,然后結(jié)合界面生成功能產(chǎn)出最終的技能編輯器界面,供策劃編輯。最后導出的技能配置數(shù)據(jù)的格式,也會結(jié)合解析出來的元數(shù)據(jù)信息進行導出和檢查。這個過程,就大致解釋了元數(shù)據(jù)的基本概念——元數(shù)據(jù)提供了其他數(shù)據(jù)的格式信息。在這個例子中,技能元數(shù)據(jù)就定義了最終編輯出來的技能數(shù)據(jù)所包含的信息,以及要在編輯器中展示這些數(shù)據(jù)所需要的信息。
這套結(jié)構(gòu)具體的信息不方便細說,但這里可以從Traits這個開源庫來看下在Python中進行數(shù)據(jù)描述的方法和形式。
Traits為Python對象的屬性增加了類型定義的功能,但除此之外還有其他的作用:
- 初始化:每個trait屬性都定義有自己的缺省值,這個缺省值用來初始化屬性驗證;
- 基于trait的屬性都有明確的類型定義,只有滿足定義的值才能賦值給屬性委托;
- trait屬性的值可以委托給其他對象的屬性監(jiān)聽;
- trait屬性的值的改變可以觸發(fā)指定的函數(shù)的運行可視化;
- 擁有trait屬性的對象可以很方便地提供一個用戶界面交互式地改變trait屬性的值。
在官方的介紹中給了一個簡單的例子描述了上面的幾個核心功能:
from enthought.traits.api import Delegate, HasTraits, Instance, Int, Strclass Parent ( HasTraits ):# 初始化: last_name被初始化為'Zhang'last_name = Str( 'Zhang' )class Child ( HasTraits ):age = Int# 驗證: father屬性的值必須是Parent類的實例father = Instance( Parent )# 委托: Child的實例的last_name屬性委托給其father屬性的last_namelast_name = Delegate( 'father' )# 監(jiān)聽: 當age屬性的值被修改時,下面的函數(shù)將被運行def _age_changed ( self, old, new ):print 'Age changed from %s to %s ' % ( old, new )這個簡單的例子展示了Traits的基本用法,在Traits中,對于每一個trait屬性都有一個與之對應的trait對象描述它。而元數(shù)據(jù)就是保存在trait對象中的額外的描述屬性用的數(shù)據(jù)。這些元數(shù)據(jù)屬性可以分為三類:
- 內(nèi)部屬性 : 這些屬性是trait對象自帶的,只讀不能寫;
- 識別屬性 : 這些屬性是可以自由地設置的,它們可以改變trait的一些行為;
- 任意屬性 : 用戶自己添加的屬性,需要自己編寫程序使用它們。
而基于這些屬性,就可以描述一份數(shù)據(jù)的各項信息,于是編寫出來的這份“代碼”,也可以被稱為元數(shù)據(jù)。
更加詳細的信息有興趣的讀者可以參考Traits的文檔描述,這里就不再贅述了。接下來我們核心來看看本次的主角——Odin插件。
3. Odin插件
Unity引擎對于自定義編輯器的支持已經(jīng)比傳統(tǒng)的游戲引擎要方便一個等級了,它基于Unity的反射機制,在界面框架內(nèi)已經(jīng)實現(xiàn)了非常方便的屬性編輯功能,比如最為常見和常用的MonoBehavior的屬性編輯和查看。相比于自研引擎要自己實現(xiàn)前文所描述的這套元數(shù)據(jù)編輯器框架,對于常規(guī)的配置需求Unity已經(jīng)做得更好了。
然而,作為開發(fā)者來說還是有更加復雜的需求是Unity這套結(jié)構(gòu)目前所不支持的,比如Dictionary的編輯,比如一些動態(tài)的顯隱控制。好在Unity有強大的Asset Store,Odin這樣的插件也就應運而生,雖然55美元的售價稍微有些貴,但我覺得它絕對物超所值!引用一段官方介紹來描述其功能:
Odin puts your Unity workflow on steroids, making it easy to build powerful and advanced user-friendly editors for you and your entire team. With an effortless integration that deploys perfectly into pre-existing workflows, Odin allows you to serialize anything and enjoy Unity with 80+ new inspector attributes, no boilerplate code and so much more!簡答來說,它通過提供更多的新屬性來方便我們編寫強大的編輯器功能,并且提供了序列化模塊。拋開序列化不說,我就舉幾個自己真正使用過的例子來描述它的一些好用功能。
3.1 字典編輯
字典的編輯這里直接給一個官方的例子:
public class DictionaryExamples : SerializedMonoBehaviour{[InfoBox("In order to serialize dictionaries, all we need to do is to inherit our class from SerializedMonoBehaviour.")]public Dictionary<int, Material> IntMaterialLookup;public Dictionary<string, string> StringStringDictionary;[DictionaryDrawerSettings(KeyLabel = "Custom Key Name", ValueLabel = "Custom Value Label")]public Dictionary<SomeEnum, MyCustomType> CustomLabels;[DictionaryDrawerSettings(DisplayMode = DictionaryDisplayOptions.ExpandedFoldout)]public Dictionary<string, List<int>> StringListDictionary;[DictionaryDrawerSettings(DisplayMode = DictionaryDisplayOptions.Foldout)]public Dictionary<SomeEnum, MyCustomType> EnumObjectLookup;[InlineProperty(LabelWidth = 90)]public struct MyCustomType{public int SomeMember;public GameObject SomePrefab;}public enum SomeEnum{First, Second, Third, Fourth, AndSoOn}}最后的編輯界面如下圖所示:
這里有比較方便的添加和刪除功能,對于復雜的數(shù)據(jù)結(jié)構(gòu),也可以采用Foldout的方式。同時這里也可以看到對于枚舉類型和自定義結(jié)構(gòu)的支持。
3.2 動態(tài)的下拉列表
為了減少使用者出錯的概率,下拉列表是一個非常常見的需求,而其中的內(nèi)容往往是動態(tài)變化的,Odin提供了ValueDropdown屬性來應對這一需求,只需要定義一個相應的獲取函數(shù)就可以了。
[LabelText("攻擊位移編號"), ValueDropdown("GetOffsetIDs")] public int offetOnAtk = -1;private IEnumerable<int> GetOffsetIDs() {List<int> oIds = new List<int>();//處理邏輯return oIds; }顯示效果如下圖所示:
3.3 錯誤提示信息
Odin也提供了豐富的信息提示功能,比如PropertyTooltip,是在鼠標在屬性名稱上懸停時顯示的tips,LabelText是最為基礎(chǔ)的顯示名稱,InfoBox可以定義單獨的提示信息,而且可以給出顯示條件,比如一個布爾值屬性或者返回為布爾值的函數(shù)。
[LabelText("技能時長"), PropertyTooltip("技能的持續(xù)時間,0表示動態(tài)技能時長"), MinValue(-1f)] [InfoBox("必須有一個觸發(fā)技能結(jié)束的位移才可以使用動態(tài)技能時長!", InfoMessageType.Error, "NoDynamicLength")] [InfoBox("技能時間乘以回能速度必須小于1!", InfoMessageType.Info)] public float length = 1.0f;提示信息的顯示效果如下圖所示:
3.4 根據(jù)條件顯示和隱藏
在編輯器中,某些屬性的是隸屬于其他屬性的,比如定義一個形狀,如果是個原型,則只需要半徑就可以了,如果是個扇形,則還需要一個角度參數(shù)。通常的解決方法要么為這兩種形狀提供不同的編輯器功能,要不就把最大范圍的屬性都顯示出來,讓使用者隨意填寫。第一種方法有時候稍顯復雜,第二種又會使編輯器使用者關(guān)注的信息膨脹,那么這時候就可以使用ShowIf或者EnableIf屬性。
[LabelText("扇形角度"), ShowIf("shapeType", BulletShapeType.扇形)] public int angle = 0;這樣就只有當shapeType是扇形的時候,才會顯示扇形角度屬性。
3.5 自動的TreeView
Odin提供的OdinMenuEditorWindow默認集成了一個TreeView列表放在左側(cè),為很多編輯器的開發(fā)提供了便利。比如官方提供的一個RPG類型游戲的編輯器Demo:
Odin插件的功能還有很多,這里就不一一列舉,有興趣的朋友可以去官網(wǎng)查看或者自己購買一份來學習和試驗。總之,借助Odin的強大功能,我原本計劃要3-5天才能完成的技能編輯器,只使用了1天時間就完成了核心框架。再加上我自己實現(xiàn)的一個簡單的C#數(shù)據(jù)導出為Lua Table的功能,基本就滿足了Demo期的核心需求。
4. 原理和簡單擴展
如果你購買了Odin插件,是可以直接獲取它的源碼的,因此也就可以一探它具體的實現(xiàn)原理了。由于是收費插件的源碼,這里就不做特別細致的探討了,總體來說,Odin就是基于兩個技術(shù)的結(jié)合來實現(xiàn)的:
- 屬性(Attributes)
- 反射(Reflection)
反射的部分比較好理解,比如ValueDropdown("GetOffsetIDs")這樣的定義中,方法的名稱使用一個字符串來進行描述,那么在最終執(zhí)行的時候,肯定是要通過反射來獲取具體的函數(shù)來執(zhí)行,并獲取返回值,這時候就要借助C#的反射機制才可以實現(xiàn)。而Odin的便利性則主要通過C#的屬性來實現(xiàn)。
微軟官方對于Attributes的定義如下:
Attributes provide a powerful method of associating metadata, or declarative information, with code (assemblies, types, methods, properties, and so forth). After an attribute is associated with a program entity, the attribute can be queried at run time by using a technique called reflection.你看,Attributes本身就是元數(shù)據(jù)的理念,它在綁定之后也是通過反射來查詢的。屬性具有如下的特點:
- 添加元數(shù)據(jù)到你的程序中,元數(shù)據(jù)在程序中是關(guān)于類型定義的信息。屬性也可以自定義;
- 屬性可以被應用于整個程序集、模塊,或者像類和屬性(Properties)這樣更小的程序單元;
- 屬性可以像方法和Properties一樣接收參數(shù);
- 借助反射,你可以查詢自己或者其他程序定義的元數(shù)據(jù)信息。
語言層面更加細節(jié)的原理不在本文的討論范圍內(nèi),我想借助我在技能編輯器實現(xiàn)時基于Attributes實現(xiàn)的一個功能來嘗試描述一下Odin的基本原理。
在實現(xiàn)從C#數(shù)據(jù)導出Lua數(shù)據(jù)功能的時候,我想嘗試優(yōu)化導出后的文件大小以避免后續(xù)技能過于復雜時對于Lua虛擬機內(nèi)存的影響。最為基本的一個優(yōu)化就是如果策劃填寫的內(nèi)容和默認值相同,就不需要導出這個數(shù)據(jù),在Lua代碼中通過a = a or default_value這種方式來獲取值即可。如果要自己在導出函數(shù)中進行實現(xiàn),或者通過一個屬性名稱白名單的數(shù)組來進行維護都會比較麻煩,Attributes是一個非常合適的功能。于是我設計了一個NotExportToLua的Attribute,它用來描述這個屬性是否要導出到Lua中,基本實現(xiàn)如下:
public class NotExportToLuaDataAttribute : System.Attribute {object defaultValue = null;public NotExportToLuaDataAttribute(){}//如果和默認值相等則無需導出public NotExportToLuaDataAttribute(Object defaultValue){this.defaultValue = defaultValue;}public bool NeedNotExportToLua(object value){if (defaultValue == null){return false;}//這里的判斷非常不嚴謹,只使用轉(zhuǎn)換為字符串的方式來判斷基礎(chǔ)類型的對象是否相等。return defaultValue.ToString() != value.ToString();} }這個Attribute支持無參數(shù)和有參數(shù)兩種形式,如果無參數(shù)則這個屬性只會在C#中使用,無需導出到Lua數(shù)據(jù)中,而如果給予參數(shù),在定義了默認值,導出時會檢查當前值對象的值是否和屬性值相同,如果相同則同樣不導出。這里為了快速實現(xiàn),使用了ToString的方式臨時實現(xiàn),并不是最正確的做法。
Type type = obj.GetType(); string indentStr = GetIndentation(indentLevel); builder.Append("{n"); bool first = true; foreach (FieldInfo f in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)){var value = f.GetValue(obj);if (!IsNotExportToLua(f, value)){//Process export.} }public static bool IsNotExportToLua(FieldInfo type, Object value) {NotExportToLuaDataAttribute attr = type.GetCustomAttribute<NotExportToLuaDataAttribute>();if (attr != null){if (!attr.NeedNotExportToLua(value)){return true;}}object[] attrs = type.GetCustomAttributes(true);for (int j = 0; j < attrs.Length; j++){Type t = attrs[j].GetType();if (t == typeof(System.ObsoleteAttribute)){return true;}}return false; }在導出的邏輯中,首先借助反射函數(shù)GetFields獲取一個對象所有的屬性,然后通過GetCustomAttribute方法獲取其身上對應類型的自定義Attribute,借助NotExportToLuaDataAttribute身上的NeedNotExportToLua來判斷是否需要導出該屬性。
這樣,在你不需要導出一個C#中定義的屬性的時候,只要為其添加[NotExportToLuaData]就可以了,這也就非常靈活地實現(xiàn)了前面的需求。Odin插件對于Attribute的定義以及通過反射獲取的數(shù)據(jù)更多,但其基本原理和我自己實現(xiàn)的這個NotExportToLuaData Attribute基本類似。
5. 總結(jié)
Meta Programming is about writing code that writes code.從元數(shù)據(jù)聊到元編程稍微有點刻意把這篇文章的立意拔高的意思,但它們兩個之間的確有著相似的理念。用數(shù)據(jù)描述數(shù)據(jù),用代碼來生成代碼,都是為了提高開發(fā)效率而“偷懶”的方法。從某個角度來說,處理元數(shù)據(jù)的代碼就是在通過對于數(shù)據(jù)的描述來減少重復代碼的編寫,也可以說它是代替程序編寫重復的代碼。
對于元數(shù)據(jù)來說,像Lua的元表一樣,她也可以有遞歸描述的能力,比如可以使用一份元數(shù)據(jù)來描述描述技能信息的元數(shù)據(jù),就是元數(shù)據(jù)的元數(shù)據(jù),通過它配合一個編輯器可以讓策劃自己定義一個技能中的數(shù)據(jù)有哪些,它們分別是什么樣的類型或者要滿足什么要的條件,這是更高層次的抽象。也許在未來,程序可以借助深度學習或者其他AI技術(shù),開發(fā)一個自己寫代碼實現(xiàn)需求的AI程序,這個開發(fā)過程,似乎可以稱之為Meta-Meta Programing……
恩,扯得有點遠了,無論元數(shù)據(jù)也好,元編程也好,起碼目前階段的核心作用都是節(jié)省程序的開發(fā)時間,或者增強程序的功能。這篇文章還講的比較淺顯,從Odin插件的使用出發(fā),稍微聊了一下元數(shù)據(jù)的基本思路和方法,無論你是否會使用到Odin插件,都希望這篇可以幫你開闊思路,從而節(jié)省一些開發(fā)時間,畢竟——“時間就是金錢,我的朋友。”
2019年7月8日晚于杭州家中
總結(jié)
以上是生活随笔為你收集整理的easyui表格编辑事件_Unity手游开发札记——从Odin插件聊基于元数据的编辑器实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于wifi的单片机无线通信研究_SKY
- 下一篇: python类的成员函数_Python实