Unity-自定義事件派發器的兩次嘗試

2022-05-28 21:07:13

一、前言:

在遊戲開發的很多時候,需要參照其他類的方法,但是一旦類多起來了,相互參照會導致參照關係混亂,極其難以閱讀。

以前初次做抖音小遊戲時,和一位經驗老道的cocos程式設計師合作,看到我寫的程式碼他不禁皺起眉頭,說我的參照關係太亂了,看不懂,但是他又不知道unity的事件派發器怎麼寫,就去網上找了一個。簡直驚豔到我了。後來在現在公司做,又見到了一種事件派發器,於是心生感慨,模仿寫了一個,並寫部落格記錄一下。

二、現在做的事件派發器

1.宣告對應的委託,此委託主要為事件用的。委託的所有返回型別都為Void,簡化派發器的複雜程度;明確委託的方法引數型別,有幾種型別就定義幾種委託。

public delegate 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?效能上如何?不同專案有不同的事件派發器,適合自己的才是最好的。如果強行將事件派發器做成那種萬金油工具,且不論他是否真的是萬金油,程式碼開發成本之大,耗費時間之長,也不是一般小遊戲公司能夠耗得起的。上文所述兩個派發器,實際上都可以用,而且經歷過實戰的,並沒有什麼大問題。