Unity-自定義事件派發器的兩次嘗試
一、前言:
在遊戲開發的很多時候,需要引用其他類的方法,但是一旦類多起來了,相互引用會導致引用關係混亂,極其難以閱讀。
以前初次做抖音小遊戲時,和一位經驗老道的cocos程式設計師合作,看到我寫的程式碼他不禁皺起眉頭,說我的引用關係太亂了,看不懂,但是他又不知道unity的事件派發器怎麼寫,就去網上找了一個。簡直驚豔到我了。後來在現在公司做,又見到了一種事件派發器,於是心生感慨,模仿寫了一個,並寫部落格記錄一下。
二、現在做的事件派發器
1.宣告對應的委託,此委託主要為事件用的。委託的所有返回型別都為Void,簡化派發器的複雜程度;明確委託的方法引數型別,有幾種型別就定義幾種委託。
publicdelegate void VoidDelegate(); public delegate void BoolDelegate(params bool[] parameters); public delegate void NumberDelegate(params float[] parameters); public delegate void GameObjectDelegate(params GameObject[] parameters);
2.儲存容器,用於儲存事件
private static List<VoidDelegate> GameStart_List;
3.監聽者,暴露給外部呼叫者的介面,對監聽者的+=或-=對應容器裡的Add和Remove
public static event VoidDelegate GameStart_Listener { add { if(value != null) { if(GameStart_List == null) { GameStart_List = new List<VoidDelegate>(1); } GameStart_List.Add(value); } } remove { if(value != null) { for(int i = 0;i<GameStart_List.Count;i++) { if(GameStart_List[i] != null && GameStart_List[i].Equals(value)) { GameStart_List.RemoveAt(i); break; } } } } }
4.派發者,因為event只能在宣告類內部Invoke,所以需要暴露給外部呼叫者介面
public static void GameStart_Dispatch() { if(GameStart_List == null || GameStart_List.Count <= 0) { return; } for(int i = 0;i < GameStart_List.Count;i++) { GameStart_List[i]?.Invoke(); } }
5.上文可看見我的事件叫GameStart,那麼我想新增一個GameEnd的事件,豈不是又要寫一遍?而且假如我的方法引數是float呢?是bool呢?是GameObject呢?豈不是改動很大?所以我在Unity做了一個自動生成事件的工具
5.1 定義ScriptObject作為配置檔案,可隨時修改以新增或者刪除事件
[CreateAssetMenu] public class EventHandlerSetting:ScriptableObject { public List<EventType> types; public List<EventItem> items; } [System.Serializable] public class EventItem { public string eventName; public string typeName; } [System.Serializable] public class EventType { public string typeName; public string typeDelegate; }
5.2 自動生成指令碼工具
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.IO; using System.Text; using System.Text.RegularExpressions; public class CodeGen { private static string SettingPath = @"EventHandleSetting"; private static EventHandlerSetting SettingObj; private static string CodePath = @"Assets\EventHandler\"; private static string CodeName = "EventHandler.cs"; private static string LineFeed = "\r\n"; private static string LineTable = "\t"; [MenuItem("Tools/EventCodeGen")] public static void CreateEventCode() { if (File.Exists(CodePath+CodeName)) { File.Delete(CodePath + CodeName); } File.Create(CodePath + CodeName).Dispose(); LoadSetting(); File.AppendAllText(CodePath + CodeName, CreateNamespace()); File.AppendAllText(CodePath + CodeName, CreateDelegate()); File.AppendAllText(CodePath + CodeName, CreateClass()); AssetDatabase.Refresh(); } /// <summary> /// 載入配置檔案 /// </summary> static void LoadSetting() { SettingObj = Resources.Load<EventHandlerSetting>(SettingPath); } static string CreateNamespace() { StringBuilder result = new StringBuilder(); result.Append("using System;" + LineFeed); result.Append("using System.Collections;" + LineFeed); result.Append("using System.Collections.Generic;" + LineFeed); result.Append("using UnityEngine;" + LineFeed+LineFeed); return result.ToString(); } /// <summary> /// 建立委託型別 /// </summary> /// <returns></returns> static string CreateDelegate() { StringBuilder result = new StringBuilder(); string commnPrefix = "public delegate void "; using (var e = SettingObj.types.GetEnumerator()) { while (e.MoveNext()) { result.Append(commnPrefix + e.Current.typeDelegate + ";" + LineFeed); } } result.Append(LineFeed); return result.ToString(); } /// <summary> /// 建立EventHandler類 /// </summary> /// <returns></returns> static string CreateClass() { StringBuilder result = new StringBuilder(); result.Append("public class EventHandler"+LineFeed); result.Append("{"+LineFeed); List<EventItem> ls = SettingObj.items; for (int i = 0; i < ls.Count; i++) { string eventName = ls[i].eventName; string eventType = ls[i].typeName; result.Append(CreateEvent(eventName, eventType)); } result.Append("}"); return result.ToString(); } /// <summary> /// 建立事件派發器 /// </summary> /// <param name="eventName"></param> /// <param name="eventType"></param> /// <returns></returns> static string CreateEvent(string eventName,string eventType) { StringBuilder result = new StringBuilder(); result.Append(LineFeed + "#region " + eventName + LineFeed); result.Append(MutiLineTable(1) + string.Format("private static List<{0}> {1}_List;", eventType + "Delegate", eventName) + LineFeed);//建立List result.Append(CreateListener(eventName, eventType)); result.Append(CreateDispatch(eventName, eventType)); result.Append("#endregion"+LineFeed); return result.ToString(); } /// <summary> /// 建立Listener /// </summary> /// <param name="eventName"></param> /// <param name="eventType"></param> /// <returns></returns> static string CreateListener(string eventName,string eventType) { StringBuilder result = new StringBuilder(); result.Append(MutiLineTable(1) + string.Format("public static event {0} {1}_Listener", eventType + "Delegate", eventName) + LineFeed); result.Append(MutiLineTable(1) + "{" + LineFeed); #region add result.Append(MutiLineTable(2) + "add" + LineFeed); result.Append(MutiLineTable(2) + "{" + LineFeed); result.Append(MutiLineTable(3) + "if(value != null)" + LineFeed); result.Append(MutiLineTable(3) + "{" + LineFeed); result.Append(MutiLineTable(4) + string.Format("if({0}_List == null)", eventName) + LineFeed); result.Append(MutiLineTable(4) + "{" + LineFeed); result.Append(MutiLineTable(5) + string.Format("{0}_List = new List<{1}>(1);", eventName, eventType + "Delegate")+LineFeed); result.Append(MutiLineTable(4) + "}" + LineFeed); result.Append(MutiLineTable(4) + string.Format("{0}_List.Add(value);", eventName) + LineFeed); result.Append(MutiLineTable(3) + "}" + LineFeed); result.Append(MutiLineTable(2) + "}" + LineFeed); #endregion #region remove result.Append(MutiLineTable(2) + "remove" + LineFeed); result.Append(MutiLineTable(2) + "{" + LineFeed); result.Append(MutiLineTable(3) + "if(value != null)" + LineFeed); result.Append(MutiLineTable(3) + "{" + LineFeed); result.Append(MutiLineTable(4) + string.Format("for(int i = 0;i<{0}_List.Count;i++)", eventName) + LineFeed); result.Append(MutiLineTable(4) + "{" + LineFeed); result.Append(MutiLineTable(5) + string.Format("if({0}_List[i] != null && {1}_List[i].Equals(value))",eventName,eventName) + LineFeed); result.Append(MutiLineTable(5) + "{" + LineFeed); result.Append(MutiLineTable(6) + string.Format("{0}_List.RemoveAt(i);" ,eventName) + LineFeed); result.Append(MutiLineTable(6) + "break;" + LineFeed); result.Append(MutiLineTable(5) + "}" + LineFeed); result.Append(MutiLineTable(4) + "}" + LineFeed); result.Append(MutiLineTable(3) + "}" + LineFeed); result.Append(MutiLineTable(2) + "}" + LineFeed); #endregion result.Append(MutiLineTable(1) + "}" + LineFeed); return result.ToString(); } /// <summary> /// 建立Dispatch /// </summary> /// <param name="eventName"></param> /// <param name="eventType"></param> /// <returns></returns> static string CreateDispatch(string eventName,string eventType) { StringBuilder result = new StringBuilder(); result.Append(MutiLineTable(1) + string.Format("public static void {0}_Dispatch({1})", eventName, GetTypeParameter(eventType)) + LineFeed); result.Append(MutiLineTable(1) + "{" + LineFeed); result.Append(MutiLineTable(2) + string.Format("if({0}_List == null || {1}_List.Count <= 0)", eventName, eventName) + LineFeed); result.Append(MutiLineTable(2) + "{" + LineFeed); result.Append(MutiLineTable(3) + "return;" + LineFeed); result.Append(MutiLineTable(2) + "}" + LineFeed); result.Append(MutiLineTable(2) + string.Format("for(int i = 0;i < {0}_List.Count;i++)", eventName) + LineFeed); result.Append(MutiLineTable(2) + "{" + LineFeed); if (!string.IsNullOrEmpty(GetTypeParameter(eventType))) { result.Append(MutiLineTable(3) + string.Format("{0}_List[i]?.Invoke(parameters[i]);", eventName) + LineFeed); } else { result.Append(MutiLineTable(3) + string.Format("{0}_List[i]?.Invoke();", eventName) + LineFeed); } result.Append(MutiLineTable(2) + "}" + LineFeed); result.Append(MutiLineTable(1) + "}" + LineFeed); return result.ToString(); } /// <summary> /// 獲取字串中括號中的內容 /// </summary> /// <param name="typeName"></param> /// <returns></returns> static string GetTypeParameter(string typeName) { if (string.IsNullOrEmpty(typeName)) { return string.Empty; } using (var e=SettingObj.types.GetEnumerator()) { while (e.MoveNext()) { if (e.Current.typeName==typeName) { string @delegate = e.Current.typeDelegate; string result = @delegate.Substring(@delegate.IndexOf("(") + 1, @delegate.IndexOf(")") - (@delegate.IndexOf("(") + 1)); Debug.Log(result); return result; } } } return string.Empty; } /// <summary> /// 多個table /// </summary> /// <param name="count"></param> /// <returns></returns> static string MutiLineTable(int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { result.Append(LineTable); } return result.ToString() ; } }View Code
6.總結
當然,我的這一套事件系統肯定還是有問題的。如果方法引數不止一個float呢?第二個引數是bool?組合起來呢?還有一個問題是,配置檔案不夠人性化,全部都是字串,假如多大一個空格或者標點就廢了。還有一個非常嚴重的問題,如果生成的指令碼,在語法上有錯,不能通過編譯器,Unity就會報錯,再次點選生成就不會生效。我認識的一個主程讓我在Unity外部生成,不要依賴Unity,且使用類似Lua、Python這種指令碼語言,目前還不會哈哈哈哈。
三、以前做的事件派發器
1.需要一個通用的引數型別,叫EventArgs,基礎型別為Systen.Object
public class EventArgs { private List<System.Object> parameters; public int Count { get { if (parameters!=null) { return parameters.Count; } else { Debug.Log("parameters is not init"); return 0; } } } public EventArgs(params System.Object[] parameters) { if (this.parameters==null) { this.parameters = new List<object>(); } for (int i = 0; i < parameters.Length; i++) { this.parameters.Add(parameters[i]); } } public System.Object this[int index] { get { if (index>=0||index<parameters.Count) { return parameters[index]; } else { Debug.LogError("index must be in range of parameters"); return null; } } } }
2.還是那句老話,事件派發器需要容器、監聽、派發三部分。
public class EventDispatcher { public delegate void Listener(EventArgs args); private static Dictionary<string, Listener> cacheEvents = new Dictionary<string, Listener>(); public static void Attach(string tag,Listener listen) { if (cacheEvents==null) { cacheEvents = new Dictionary<string, Listener>(); } if (cacheEvents.ContainsKey(tag)) { Debug.LogWarning("this tag already exsit in cache,please check agin,tag name:" + tag); return; } if (listen==null) { Debug.LogWarning("listen is null,cache failed"); return; } cacheEvents.Add(tag, listen); } public static void Detach(string tag) { if (!cacheEvents.ContainsKey(tag)) { Debug.LogWarning("tag is not exsit in cache,tag name:"+tag); return; } cacheEvents.Remove(tag); } public static void Dispatch(string tag,EventArgs args) { if (!cacheEvents.ContainsKey(tag)) { Debug.LogWarning("this tag does not exsit in cache,please check agin,tag name:" + tag); return; } if (cacheEvents[tag]==null) { Debug.LogWarning("this listen is null,invoke failed"); return; } cacheEvents[tag].Invoke(args); } }
3.總結:
此事件派發器也存在缺陷,任何方法的引數型別都會被轉換成System.Object型別,有多餘的封裝箱操作。且不能重複新增一個方法,至少他的tag不能一樣。程式碼可讀性差,報錯了都不知道在哪兒。
四、關於事件派發器自己的看法
我相信沒有完美的派發器這一說,好的派發器與壞的派發器區別在於,呼叫是否方便?會不會存在隱藏的危險bug?效能上如何?不同專案有不同的事件派發器,適合自己的才是最好的。如果強行將事件派發器做成那種萬金油工具,且不論他是否真的是萬金油,程式碼開發成本之大,耗費時間之長,也不是一般小遊戲公司能夠耗得起的。上文所述兩個派發器,實際上都可以用,而且經歷過實戰的,並沒有什麼大問題。