• Unity-自定义事件派发器的两次尝试


    一、前言:

    在游戏开发的很多时候,需要引用其他类的方法,但是一旦类多起来了,相互引用会导致引用关系混乱,极其难以阅读。

    以前初次做抖音小游戏时,和一位经验老道的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?性能上如何?不同项目有不同的事件派发器,适合自己的才是最好的。如果强行将事件派发器做成那种万金油工具,且不论他是否真的是万金油,代码开发成本之大,耗费时间之长,也不是一般小游戏公司能够耗得起的。上文所述两个派发器,实际上都可以用,而且经历过实战的,并没有什么大问题。

     

  • 相关阅读:
    rk3588硬件构成-rock5b
    工厂模式(Factory Pattern) 与抽象工厂模式(Abstract Factory Pattern)
    微信小程序开发---事件的绑定
    uni-app - 树形结构选择器组件(支持单选 / 多选 / 可选择父级 / 弹框形式)树形控件 tree 无限级节点树,树插件完整源码,最好用的组件教程
    Huggingface transformers 里的模型加载的两种方式的写法
    WSL2-ubuntu18.04配置笔记 :6Rstudioserver配置
    LeetCode(25)验证回文串【双指针】【简单】
    记一场vue面试
    ERP系统选型需谨慎
    三、C语言常用运算符
  • 原文地址:https://www.cnblogs.com/JunJiang-Blog/p/16321562.html