Unity学习笔记:基础框架之事件中心

前言

本篇为学习总结性质的文章,若有任何问题或错误,欢迎在评论区指出。
如果本文对您有一定帮助,也欢迎点赞、收藏、关注。

本文前置知识点:字典,委托,单例模式,万物基类object,里氏替换原则,观察者设计模式

目录

  • 前言
  • 引入
  • 思路
    • 一、监听一个事件
    • 二、整合多个事件
    • 三、完善
      • 关于Remove方法
      • 关于Clear方法
      • 关于传参
    • 四、优化
  • 源码

引入

游戏中经常会有这样的逻辑需求:玩家击杀怪物,从而获得经验值,完成某些任务,同时也为一些成就增加进度等等。
试问,如果你要去实现这样的功能,你会怎么做?
可能有些人会觉得很简单,无非就是在怪物死亡逻辑中添加对应的需求罢了,例如:

public class Monster 
{int HP;int ATK;int DEF;//略……public void Atack() { }public void Skill_1() { }//略……public void Dead(){Debug.Log("死亡动画和销毁");//略……Debug.Log("调用玩家增加经验的对应函数");Debug.Log("调用对应任务累计进度的函数");Debug.Log("调用对应成就累计进度的函数");Debug.Log("其他逻辑");}
}

这样一写出来大家可能就会意识到问题所在——程序的耦合度太高
如果作为GameJam制作的小游戏还好,但一旦项目大了,尤其是游戏拥有多种系统、策划增加各种需求后,整个游戏各个模块间会疯狂交织,变得异常复杂,修改和维护的成本就会急剧攀升。

在这里插入图片描述
如何解决这个问题?
我们可以建立一个“中转模块”。每次某些事件发生时,可以仅通知“中转模块”,再令其去处理该事件发生所导致的相应结果。
这样不仅降低了程序耦合度,还使逻辑更加清晰明了,日后维护与修改的难度大大降低
并且,因为有了这样的一个中转模块,协同开发时可以各自分离,效率可以大幅提高
在这里插入图片描述

这种集中管理和处理事件的模块,我们可以将其称为“事件中心”。

思路

一、监听一个事件

如何写一个“事件中心”呢?
如上文所述,我们需要做的,是建立一个“中转站”。一些事件发生时(如“怪物死亡“),我们需要告诉“中转站”,让它去处理该事件所引发的所有结果(如“玩家获得经验值”、“累计任务进度”等等)。
当然,程序是不可能知道“怪物死亡”等事件发生时需要处理哪些“结果”的,所以我们需要在最开始便为一个事件设定好其所有结果。换句话说,就是每个结果都去监听对应事件的发生
说到这里,大家可能就会有思路了:定义一个委托,在游戏开始时将所有“结果”要处理的逻辑函数放入委托中,在事件真正发生时再执行这个委托,从而结算所有结果。
大致思路如下图:
在这里插入图片描述

代码较为简单,就不列出了。

二、整合多个事件

如上,我们就可以简单地通过委托来低耦合度地处理事件。
但是我们真正要实现的是“事件中心”,是可以处理多种事件的。我们也不希望每有一个事件就申明一个对应的委托,毕竟就算是一个简单的小游戏,其中的事件也是非常非常多的。
我们想要一个事件对应一个委托函数,并且集中地将它们存储起来。说到这里我们需要的东西就很明显了——字典
在这里插入图片描述

我们可以用字符串作键,用Unity自带委托作值。事件中心也只会存在一个,可以写成单例模式
代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events; //为了使用Unity自带委托,必须要引用public class EventCenter
{//单例模式private static EventCenter instance;public static EventCenter GetInstance(){if (instance == null)instance = new EventCenter();return instance;}//key对应事件名,value对应“结果”(需要监听对应事件的委托函数) private Dictionary eventDic = new Dictionary();/// ///添加事件监听 /// /// 事件名/// “结果”(需要监听对应事件的委托函数)public void AddEventListener(string name, UnityAction action){//判断字典里是否已有对应事件和委托if (eventDic.ContainsKey(name)){//已有则直接添加eventDic[name] += action;}else{//没有则新加键值对eventDic.Add(name, action);}}/// /// 事件触发/// /// 需要触发的事件名public void EventTrigger(string name){if (eventDic.ContainsKey(name)){eventDic[name]();//eventDic[name].Invoke(); //另一种执行方法}}
}

以上,我们就完成了“事件中心”最核心的逻辑。
我们可以同时处理多个事件,使用AddEventListener()为不同事件添加监听,并在其发生时调用EventTrigger()使对应事件的所有“结果”得到结算。

三、完善

如果直接使用以上代码作各种事件的处理还会有些许问题。

关于Remove方法

首先,仍然以“玩家打怪”这个事件为例。
我们如果使用以上事件中心,在玩家管理脚本Player的Awake或Start生命周期中,调用AddEventListener()监听了怪物死亡。
然后,玩家正常游玩游戏,在此过程中玩家角色死亡,Player这个对象被销毁了。但直到此时,事件中心的“怪物死亡”这个委托仍然和Player中的某个“结果”函数建立着联系。
我们如果在Player被销毁的那一刻没有去取消它对“怪物死亡”这个事件的监听,那么这种引用关系就会一直存在。在往后GC时,由于有这个引用关系的存在,Player这个对象也就永远不会被释放,导致内存泄漏

因此,与AddEventListener相对应的,事件中心也应该有一个RemoveEventListener方法,用于移除事件监听。
逻辑其实很简单,就是查找字典中对应的委托函数,从中移除不需要监听的部分:

    //移除事件监听public void RemoveEventListener(string name,UnityAction action){if (eventDic.ContainsKey(name)){eventDic[name] -= action;}}

关于Clear方法

同理,既然游戏中可能导致单个事件监听需要被移除,那自然会有需要移除所有事件监听的情况。
比如,当我们切换场景时,因为原场景的所有对象都被移除掉了,原有的事件监听自然会全部失去意义。
所以,除了移除单个监听的RemoveEventListener方法,我们也需要一个方法来清空事件中心。
具体逻辑也就是清空事件中心的字典:

   //清空事件中心public void Clear(){eventDic.Clear();}

关于传参

到此,我们这个“事件中心”还有问题吗?还有。
实际去使用,会发现:每次我们要处理对象不同但逻辑相同的“结果”时,不断地复制、粘贴、替换就会显得很蠢且繁琐。
所以要写一个好用的事件中心,我们需要其所带委托能够传参。
使用什么参数呢?
显然,使用万物基类object是通用性最高的。并且就算有不止一个参数,我们也可以往object里装载数组,实现多参数的传递。

完善后的代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;public class EventCenter
{//单例模式private static EventCenter instance;public static EventCenter GetInstance(){if (instance == null)instance = new EventCenter();return instance;}//使用参数为object的Unity自带委托作字典的值private Dictionary> eventDic = new Dictionary>();/// ///添加事件监听 /// /// 事件名/// “结果”(需要监听对应事件的委托函数)public void AddEventListener(string name, UnityAction action){if (eventDic.ContainsKey(name)){eventDic[name] += action;}else{eventDic.Add(name, action);}}//移除事件监听public void RemoveEventListener(string name,UnityAction action){if (eventDic.ContainsKey(name)){eventDic[name] -= action;}}/// /// 事件触发/// /// 需要触发的事件名/// 参数public void EventTrigger(string name,object info){if (eventDic.ContainsKey(name)){eventDic[name](info);//或使用://eventDic[name].Invoke(info);}}//清空事件中心public void Clear(){eventDic.Clear();}
}
 

四、优化

其实完成了以上完善步骤后,该“事件中心”可以说是没有明显缺陷了,也能基本满足各种需求。
但我们还能对其进行进一步的优化。
首先,使用object作为委托的参数,增加了可适用性,但如果用其装载值类型(虽然概率较小),无疑会有object装箱拆箱的额外开销。这是一个我们可以优化的点。

如何优化?
答:使用泛型。

但其实并不能直接把字典的值从UnityAction 直接变为 UnityAction, 这样的话拥有泛型成员的EventCenter不得不成为一个泛型类,最后变成只能存储一种有参委托的事件中心,与我们最初的想法背道而驰。
正确且高效的做法是,定义一个空接口,使其作为一个再次封装后的“UnityAction”的父类,这样由里氏替换原则,就可以用该空接口作为字典的值,存储T为各种类型的UnityAction
详见下:

//空接口
public interface IEventInfo
{}//对UnityAction进行封装,继承空接口
public class EventInfo : IEventInfo
{public UnityAction actions;public EventInfo( UnityAction action){actions += action;}
}//因为有些事件确实不需要参数,可以封装一个无参委托来继承空接口
//这样也能让事件中心更好用
public class EventInfo : IEventInfo
{public UnityAction actions;public EventInfo(UnityAction action){actions += action;}
}

这样,事件中心中的字典就可以写为:

private Dictionary eventDic = new Dictionary();

源码

经过以上优化,修改代码后的最终版本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events; //为了使用Unity自带委托,必须要引用public interface IEventInfo
{}public class EventInfo : IEventInfo
{public UnityAction actions;public EventInfo( UnityAction action){actions += action;}
}public class EventInfo : IEventInfo
{public UnityAction actions;public EventInfo(UnityAction action){actions += action;}
}/// 
/// 事件中心
/// 
public class EventCenter
{//单例模式private static EventCenter instance;public static EventCenter GetInstance(){if (instance == null)instance = new EventCenter();return instance;}private Dictionary eventDic = new Dictionary();//添加事件监听public void AddEventListener(string name, UnityAction action){if( eventDic.ContainsKey(name) ){(eventDic[name] as EventInfo).actions += action;}else{eventDic.Add(name, new EventInfo( action ));}}//监听不需要参数传递的事件public void AddEventListener(string name, UnityAction action){if (eventDic.ContainsKey(name)){(eventDic[name] as EventInfo).actions += action;}else{eventDic.Add(name, new EventInfo(action));}}//移除对应的事件监听public void RemoveEventListener(string name, UnityAction action){if (eventDic.ContainsKey(name))(eventDic[name] as EventInfo).actions -= action;}//移除不需要参数的事件public void RemoveEventListener(string name, UnityAction action){if (eventDic.ContainsKey(name))(eventDic[name] as EventInfo).actions -= action;}//事件触发public void EventTrigger(string name, T info){if (eventDic.ContainsKey(name)){if((eventDic[name] as EventInfo).actions != null)(eventDic[name] as EventInfo).actions.Invoke(info);}}//事件触发(不需要参数的)public void EventTrigger(string name){if (eventDic.ContainsKey(name)){if ((eventDic[name] as EventInfo).actions != null)(eventDic[name] as EventInfo).actions.Invoke();}}//清空事件中心public void Clear(){eventDic.Clear();}
}

自此,这样的“事件中心”已能基本满足各种需求。
如果各位有其他补充,欢迎在评论区讨论。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章