08 事件管理器

上一节我们写了一个简单的引用池,而单一的引用池是没有啥作用的,事件池就是引用池的作用之一

我们当然可以把事件定义在模块的内部,比如把一个打开UI的事件定义在UI模块内,但是随着我们模块的不断增多,如果所有事件都定义在自己的模块内,在后期是很难维护的,我们都不知道一共定义了哪些事件…因此,一个全局的事件管理中心,是很有必要的

事件池

事件池是实际上保存事件的地方,可以添加或者取消订阅事件。

在此之前,我希望我们的委托遵循.NET规范,即应该是如下格式

 public delegate void TestEventHandler(Object sender, EventArgs e);

其中sender是事件的发送者,而e则是发送的数据

发送者自然是之后会写到的事件管理器,而这里的数据类型我们需要自定义,提供一个统一的基类

/// 所有事件的基类,会被引用池管理
public abstract class GlobalEventArgs:IReference
{
        public abstract int Id { get; set; }
        public abstract void Clear();
}

其中ID代表事件类型,不同类型的事件需要有不同的ID。Clear()方法属于IReference,我们会发现这个数据类型继承自引用接口,这说明什么?说明事件池传递的参数,是被引用池管理的!这就是引用池的作用之一,如果没有引用池,那每一次事件的调用我们都需要主动的new一个类型。

接着写好基本的事件池框架

public class EventPool<T> where T : GlobalEventArgs
{
#region Private
        /// 事件编码与对应的处理方法,这里的key,就是我们注册在事件数据里的ID,EventHandler<T>是一个(Object,T)类型的委托
        private Dictionary<int, EventHandler<T>> m_EventHandlers;
#endregion

#region 构造方法
        public EventPool()
        {
            m_EventHandlers = new Dictionary<int, EventHandler<T>>();
        }
#endregion
}

接口方法,添加订阅与取消订阅

public class EventPool<T> where T : GlobalEventArgs
{
#region Public 接口方法
        /// 订阅事件
        public void Subscribe(int id, EventHandler<T> handler)
        {
            if (handler == null)
            {
                throw new Exception("事件处理方法为空,无法订阅...");
            }
 
            EventHandler<T> eventHandler = null;
            
            // 检查是否获取处理方法失败或获取到的为空
            if (!m_EventHandlers.TryGetValue(id, out eventHandler) || eventHandler == null)
            {
                m_EventHandlers[id] = handler;
            }
            // 不为空,就检查处理方法是否重复了
            else if (Check(id, handler))
            {
                Debug.LogError("ID为:" + (SGFEvents)id + "的事件下已存在这个处理方法:" + nameof(handler) + "...");
            }
            else
            {
                eventHandler += handler;
                m_EventHandlers[id] = eventHandler;
            }
        }
 
        /// 取消订阅事件
        public void Unsubscribe(int id, EventHandler<T> handler)
        {
            if (handler == null)
            {
                throw new Exception("事件处理方法为空,无法取消订阅...");
            }
 
            if (m_EventHandlers.ContainsKey(id))
            {
                m_EventHandlers[id] -= handler;
            }
        }

        
        /// 抛出事件(线程不安全),抛出之后会立刻执行
        public void FireNow(object sender, T e)
        {
            HandleEvent(sender, e);
        }
#endregion
    
#region Private 工具方法 
        /// 检查某个编码的事件是否存在它对应的处理方法
        private bool Check(int id, EventHandler<T> handler)
        {
            if (handler == null)
            {
                throw new Exception("事件处理方法为空...");
            }
 
            EventHandler<T> handlers = null;
            if (!m_EventHandlers.TryGetValue(id, out handlers))
            {
                return false;
            }
 
            if (handlers == null)
            {
                return false;
            }
 
            // 遍历委托里的所有方法
            foreach (EventHandler<T> i in handlers.GetInvocationList())
            {
                if (i == handler)
                {
                    return true;
                }
            }
 
            return false;
        }
        
        /// 事件处理
        private void HandleEvent(object sender, T e)
        {
            // 尝试获取事件的处理方法
            int eventId = e.Id;
            EventHandler<T> handlers = null;
            if (m_EventHandlers.TryGetValue(eventId, out handlers))
            {
                if (handlers != null)
                {
                    handlers(sender, e);
                }
                else
                {
                    throw new Exception("事件没有对应处理方法:" + eventId);
                }
            }
        }
#endregion
}

到此为止大体上的内容就已经完成了,我们来写我们的事件管理器

事件管理器

其实事件管理器就是对事件池的一个代理罢了

public class EventManager : ManagerBase
{
#region Private
        /// 事件池,维护一个事件池,其实事件管理器就是对事件池的代理
        private EventPool<GlobalEventArgs> m_EventPool;
#endregion

#region 构造方法
        public EventManager()
        {
            m_EventPool = new EventPool<GlobalEventArgs>();
        }
#endregion
        
#region Override
        public override int Priority
        {
            get { return ManagerPriority.EventManager.GetHashCode(); }
        }
#endregion

#region Public 接口方法
        /// 订阅事件
        public void Subscribe(SGFEvents id, EventHandler<GlobalEventArgs> handler)
        {
            // =========== 这一部分是用于DEBUG的,会在Hierachy中显示出当前的引用状态 ========== 
            #if UNITY_EDITOR
            var trans = SGFEntry.Instance.transform.Find("EventManager");
            var name = id.ToString();
            var temp = trans.Find(name);
            if (temp == null)   
            {
                GameObject go = new GameObject();
                go.name = name;
                go.transform.SetParent(trans);
                
                GameObject go2 = new GameObject();
                go2.name = $"{handler.Target}:{handler.Method.Name}";
                go2.transform.SetParent(go.transform);
            }
            else
            {
                GameObject go2 = new GameObject();
                go2.name = $"{handler.Target}:{handler.Method.Name}";
                go2.transform.SetParent(temp.transform);
            }
            #endif
            // =========================================================================
            
            m_EventPool.Subscribe(id.GetHashCode(), handler);
        }
 
        /// 取消订阅事件
        public void Unsubscribe(SGFEvents id, EventHandler<GlobalEventArgs> handler)
        {
            m_EventPool.Unsubscribe(id.GetHashCode(), handler);
            
            // =========== 这一部分是用于DEBUG的,会在Hierachy中显示出当前的引用状态 ==========
			#if UNITY_EDITOR
            var trans = SGFEntry.Instance.transform.Find("EventManager");
            var group = trans.Find(id.ToString());
            var child = group.Find($"{handler.Target}:{handler.Method.Name}");
            if (child != null) 
            {
                GameObject.DestroyImmediate(child.gameObject);
            }
            if (group.childCount <= 0) 
            {
                GameObject.DestroyImmediate(group.gameObject);
            }
			#endif
            // =========================================================================
        }
        
        /// 抛出事件(线程不安全)
        public void FireNow(object sender, GlobalEventArgs e)
        {
            m_EventPool.FireNow(sender, e);
        }
#endregion
}

完全都是在调用事件池中的方法,单纯只是一个代理类而已

好了让我们先来测试一下,以打开UI为例,首先我们需要有一个打开UI的数据类型,且必须继承自GolbalEventArgs

1

public class UIOpenEventArgs : GlobalEventArgs
{
    /// UI的名字,所有的UI都会注册这个事件,则需要靠名称来确定当前到底打开了哪一个UI
    public string uiName;
    
    public string data;

    /// 这个ID是事件的ID,会根据事件ID找到相应的处理方法 
    public override int Id
    {
        get
        {
            return SGFEvents.OpenUI.GetHashCode();
        }
        set { }
    }

    /// 归还引用后清空数据
    public override void Clear()
    {
        Id = UIRegister.None.GetHashCode();
        uiName = string.Empty;
        data = string.Empty;
    }
}

然后让我们在UI中注册这个事件,打开UI这个事件应该是所有UI都需要的,所以我们直接在UIBase中注册

在此之前,为了事件方便管理,我们把所有事件的ID注册到一个类中

/// 注册所有事件的事件编码
public enum SGFEvents
{
        /*================== UI相关 =================*/
        OpenUI,
}

更新UIBase

public abstract class UIBase : MonoBehaviour
{
#region Field
        /// 事件管理器
        private EventManager eventManager;
        /// UI的名字,不可重复
        public string uiName;
#endregion
    
#region MonoBehaviour
        private void Awake()
        {
            eventManager = SGFEntry.Instance.GetManager<EventManager>();
            eventManager.Subscribe(SGFEvents.OpenUI,AfterShow);
            Load();
        }

        private void OnDestroy()
        {
            eventManager.Unsubscribe(SGFEvents.OpenUI,AfterShow);
            UnLoad();
        }
#endregion


#region 工具方法
        private void AfterShow(object o, GlobalEventArgs e)
        {
            var temp = e as UIOpenEventArgs;
            if (!temp.uiName.Equals(uiName))
            {
                return;
            }
            DoAfterShow(o,temp);
        }
#endregion
    
#region 事件
        /// <summary>
        /// 会在这个UI显示后调用 
        /// </summary>
        public virtual void DoAfterShow(object o, UIOpenEventArgs e)
        {
        
        }
#endregion
}

更新UIManager

public class UIManager : ManagerBase
{
#region Field
        /// 事件管理器
        private EventManager eventManager;
        /// 引用池
        private ReferenceManager referenceManager;
#endregion

#region 管理器生命周期
        public override void Init()
        {
            //...
        
            // 初始化管理器
            eventManager = SGFEntry.Instance.GetManager<EventManager>();
            referenceManager = SGFEntry.Instance.GetManager<ReferenceManager>();
        }
#endregion

 #region 接口方法
        /// <summary>
        /// 打开一个UI
        /// </summary>
        public void Open(UIStruct data, UIOpenEventArgs uiOpenEventArgs)
        {
            // 如果这个UI还没被加载,那需要先加载
            if (!uiLoaded.TryGetValue(data.name,out var ui)) 
            {
                LoadUI(data,out ui);
            }
            // 显示UI
            ShowUI(ui);
            // 发送打开UI事件
            uiOpenEventArgs.uiName = ui.uiName;
            eventManager.FireNow(this,uiOpenEventArgs);
        }
#endregion
}

以Fixed1这个UI为例

public class Fixed1 : UIBase
{
    public override void DoAfterShow(object o, UIOpenEventArgs e)
    {
        Debug.Log(e.data);
    }
}

public class test : MonoBehaviour
{
    private ReferenceManager referenceManager;
    void Start()
    {
        referenceManager = SGFEntry.Instance.GetManager<ReferenceManager>();
        var tempRef = referenceManager.Acquire<UIOpenEventArgs>();
        tempRef.data = "测试:打开UI的事件!";
        SGFEntry.Instance.GetManager<UIManager>().Open(UIs.Fixed1, tempRef);
    }
}

1

大体上算是完成了,但会有一些细节可能会影响大家的需求,在我这里,我把所有的打开UI归类为一个委托,不关我打开A还是打开B还是打开C,所有注册了这个事件的页面,都会被执行,所以如果大家只想当前最新打开的页面执行委托,那么在重写DoAfterShow方法时,需要进行额外的判断,根据传入的数据来判断是不是当前页面。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ource Type VolumeSnapshot by Name new-snapshot-demo: snapshot new-snapshot-demo not bound I0714 08:37:27.829848 1 event.go:282] Event(v1.ObjectReference{Kind:"PersistentVolumeClaim", Namespace:"default", Name:"snapshot-demo-restore", UID:"8dd75b4b-9a16-4940-b078-7088a672a649", APIVersion:"v1", ResourceVersion:"2855605", FieldPath:""}): type: 'Warning' reason: 'ProvisioningFailed' failed to provision volume with StorageClass "evs-sc": error getting handle for DataSource Type VolumeSnapshot by Name new-snapshot-demo: snapshot new-snapshot-demo not bound I0714 08:38:13.719937 1 controller.go:1279] provision "default/snapshot-demo-restore" class "evs-sc": started I0714 08:38:13.720202 1 event.go:282] Event(v1.ObjectReference{Kind:"PersistentVolumeClaim", Namespace:"default", Name:"snapshot-demo-restore", UID:"8dd75b4b-9a16-4940-b078-7088a672a649", APIVersion:"v1", ResourceVersion:"2868531", FieldPath:""}): type: 'Normal' reason: 'Provisioning' External provisioner is provisioning volume for claim "default/snapshot-demo-restore" W0714 08:38:13.723506 1 controller.go:933] Retrying syncing claim "8dd75b4b-9a16-4940-b078-7088a672a649", failure 31 E0714 08:38:13.723534 1 controller.go:956] error syncing claim "8dd75b4b-9a16-4940-b078-7088a672a649": failed to provision volume with StorageClass "evs-sc": error getting handle for DataSource Type VolumeSnapshot by Name new-snapshot-demo: snapshot new-snapshot-demo not bound I0714 08:38:13.723562 1 event.go:282] Event(v1.ObjectReference{Kind:"PersistentVolumeClaim", Namespace:"default", Name:"snapshot-demo-restore", UID:"8dd75b4b-9a16-4940-b078-7088a672a649", APIVersion:"v1", ResourceVersion:"2868531", FieldPath:""}): type: 'Warning' reason: 'ProvisioningFailed' failed to provision volume with StorageClass "evs-sc": error getting handle for DataSource Type VolumeSnapshot by Name new-snapshot-demo: snapshot new-snapshot-demo not bound
最新发布
07-15

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值