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
四、优化
其实完成了以上完善步骤后,该“事件中心”可以说是没有明显缺陷了,也能基本满足各种需求。
但我们还能对其进行进一步的优化。
首先,使用object作为委托的参数,增加了可适用性,但如果用其装载值类型(虽然概率较小),无疑会有object装箱拆箱的额外开销。这是一个我们可以优化的点。
如何优化?
答:使用泛型。
但其实并不能直接把字典的值从UnityAction
正确且高效的做法是,定义一个空接口,使其作为一个再次封装后的“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();}
}
自此,这样的“事件中心”已能基本满足各种需求。
如果各位有其他补充,欢迎在评论区讨论。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
