前言:
在游戏运行中,玩家的一些交互动作会产生大量的事件,在游戏体量比较小时,我们还可以对他们一一的管理,但是当游戏规模到达一定的程度,再对他们分别处理,就会产生大量的耦合,使得游戏复杂混乱,难以开发和维护,因此需要一个专门的事件管理器帮助游戏开发者进行游戏开发
事件管理器概念理解
为啥需要事件管理器:
举个例子,为了在有限的输入变量中,产生尽可能多的控制变量。就要游戏的不同状态下,一个输入变量有不同的控制变量产生。比如在不同角色的运动控制上,我们希望点击鼠标的左键会有不同的效果的,比如下面的这种情况(仅为例子,一般不会有这种情况):
- 点击鼠标左键:第一种情况下可以跳跃,第二种情况下可以攻击
- 点击键盘空格:第一种情况下可以攻击,第二种情况下可以跳跃
由于跳跃与攻击都需要调用两次,所以我们把他们提取出来,写成方法,方便多次调用。
这里就产生了事件的概念,跳跃与攻击可以作为执行事件,而点击鼠标和键盘就是触发执行事件的触发事件,为了实现游戏目的,我们只需要将执行事件添加给触发事件即可,当玩家点击鼠标,激活触发事件,然后调用执行事件,完成操作
这样看起来很简单,但是如果一个游戏中有大量的事件呢,该怎么管理这些事件才能更加简单便捷呢?
关于管理:
- 本文中说到的管理,是如何更加方便的在无数的触发事件与执行事件中找到其中任何一个触发事件与执行事件的联系
回到主题,就是事件管理器,可以控制多个触发事件与执行事件的关系,可以用下面的图来看:
至于如果管理这些事件的注册、联系、执行则要在后续步骤完成了
生命周期:
我大概将事件管理器分为两个方面(仅仅个人理解):
- 触发事件:类似于点击鼠标、键盘等操作
- 执行事件:类似于点击鼠标后需要执行的事件,比如页面跳转
而事件管理器的主要的四部分则是:
- 订阅事件:用来处理触发事件,可以理解为将需要执行的事件存储在一个容器内(本事件管理器使用字典作为容器),并给其一个
ID
(用字符串来表示) - 注销事件:当游戏结束时,需要删除这些事件,释放内存,或者某些事件再也用不到了,也可以删除
- 触发事件:通过输入触发事件与触发事件对应的执行事件的
ID
,来执行执行事件 - 清除事件:如果某个触发事件用不到了,则直接清除其对应的执行事件并删除
ID
注意:
- 关于
ID
,即代表某一个触发事件,而如果某一个执行事件与这个触发事件对应,则将这个ID
赋给执行事件
事件管理器脚本编写
单例模式:
在事件管理器之前,需要先了解一下单例模式,对于Unity
,单例模式有两种形态,即继承于MonoBehaviour
的单例模式与不继承于MonoBehaviour
的单例模式
而对于单例模式本身也有很多种,懒汉模式、饿汉模式等等,其中涉及到多线程的安全问题,但是由于Unity
对于多线程的弱支持,一般就不考虑多线程的问题,因此学一个能用的就行,不需要记太多
对于继承于MonoBehaviour
的脚本之前的文章有用到过,但是不完善,今天再来一次:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainTest : MonoBehaviour
{
//声明单例模式
static MainTest _instance;
private void Awake()
{
if (_instance != null)
{
Destroy(this);
}
_instance = this;
}
/// <summary>
/// 第一种:使用静态使得外部脚本可以直接调用方法
/// </summary>
public static void Print()
{
Debug.Log("这是一个单例测试");
}
/// <summary>
/// 第二种,使用public对外暴漏唯一实例,然后可以调用该实例的方法
/// </summary>
/// <returns></returns>
public static MainTest instance()
{
return _instance;
}
}
而不继承于MonoBehaviour
的单例模式是今天要使用的,首先在定义时直接实例出一个对象,然后使用get
方法对外暴露这个唯一的实例:
public class EvenManager
{
static EvenManager evenManager = new EvenManager();
private EvenManager(){}
public static EvenManager Instance
{
get
{
return evenManager;
}
}
}
事件管理器实现代码
说了那么多,终于来到正题了,由于代码中有大量的注释,所以就不过多的解释,直接看代码就完事了:
注意:
- 本代码是学习优快云大佬林新发的代码而写的,大家可以去看看他写的博客,真的很有技术含量,可以学到很多
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EvenManager
{
/*--------声明单例类---------*/
static EvenManager evenManager = new EvenManager();
private EvenManager(){}
public static EvenManager Instance
{
get
{
return evenManager;
}
}
//执行事件,参数为对象列表
public delegate void EventDelegate(object[] args);
//使用字典格式(因为会有多个执行事件)存储执行事件,并用字符串存储ID
private Dictionary<string, Dictionary<int, EventDelegate>> eventListeners = new Dictionary<string, Dictionary<int, EventDelegate>>();
/*-------事件生命周期---------*/
/// <summary>
/// 订阅事件,即存储一组执行事件
/// </summary>
/// <param name="eventName">一个触发事件对应的所有执行事件的ID</param>
/// <param name="handler">执行事件</param>
public void Regist(string eventName,EventDelegate handler)
{
//如果要存储执行事件为空,不处理
if(handler==null)
{
return;
}
//如果事件字典中不包含该eventName这个键,则创建一个键值对(即一个新的触发事件)
if(!eventListeners.ContainsKey(eventName))
{
eventListeners.Add(eventName, new Dictionary<int, EventDelegate>());
}
//获取eventName键对应的值,也同样是一个字典类型
var handlerDic = eventListeners[eventName];
//获取输入的委托事件的哈希值(字典索引的一种方式)
var handlerHash = handler.GetHashCode();
//如果输入事件对应的字典中包含执行事件,则从中移除
if(handlerDic.ContainsKey(handlerHash))
{
handlerDic.Remove(handlerHash);
}
//将eventName键对应的值(输入的执行事件)的字典类型(用handler的哈希值作为键)加入到事件字典中
eventListeners[eventName].Add(handler.GetHashCode(), handler);
}
/// <summary>
/// 注销事件:在触发事件对应的多个执行事件中删除不再需要的执行事件
/// </summary>
/// <param name="eventName"></param>
/// <param name="handLer"></param>
public void UnRegist(string eventName,EventDelegate handLer)
{
//同样,如果执行事件为空,不进行后续处理
if(handLer==null)
{
return;
}
//如果事件字典中包含输eventName这个ID,则移除该键中对应的字典中的输入的执行事件
if (eventListeners.ContainsKey(eventName))
{
eventListeners[eventName].Remove(handLer.GetHashCode());
//在移除后,如果eventName对应的字典为空,或者不存在,则删除该键值对
if(eventListeners[eventName]==null||eventListeners[eventName].Count==0)
{
eventListeners.Remove(eventName);
}
}
}
/// <summary>
/// 触发事件:根据ID来找到触发事件对应的所有执行事件并运行
/// </summary>
/// <param name="eventName"></param>
/// <param name="objs"></param>
public void DispatchEvent(string eventName,params object[] objs)
{
// 如果包含eventName这个ID
if (eventListeners.ContainsKey(eventName))
{
//获取eventName键对应的所有执行事件
var handlerDic = eventListeners[eventName];
if(handlerDic!=null&&handlerDic.Count>0)
{
var dic = new Dictionary<int, EventDelegate>(handlerDic);
//通过对eventName键对应的所有执行进行遍历,然后运行
foreach(var f in dic.Values )
{
try
{
//执行所有的委托事件,即EventDelegate(object[] args)
f(objs);
}
catch (System.Exception)
{
throw;
}
}
}
}
}
/// <summary>
/// 删除事件:与注销事件不同,删除事件直接删除触发事件所对应的所有执行事件,同时删除这个触发事件
/// </summary>
/// <param name="eventName"></param>
public void ClearEvents(string eventName)
{
//如果事件字典中包含eventName中的键,则移除触发事件对应的所有执行事件
if(eventListeners.ContainsKey(eventName))
{
eventListeners.Remove(eventName);
}
}
}
无论学习还是直接用都是可以的,本人经过测试的
调用与执行事件管理器:
首先就是执行事件的注册,首先写一个字符串作为触发事件的ID
,然后把所有执行事件添加这个ID
对应的字典中,这样就完成了一个触发事件的注册
,而事件的注销没有啥好讲的,就是卸磨杀驴,用完就扔。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RunTest : MonoBehaviour
{
//定义一个字符串作为触发事件的ID
public const string EVENT_ONCOLICK = "EVENT_ONCOLICK";
private void Awake()
{
//注册事件
EvenManager.Instance.Regist(EVENT_ONCOLICK, Print);
}
private void OnDestroy()
{
//注销事件
EvenManager.Instance.UnRegist(EVENT_ONCOLICK, Print);
}
void Print(params object[] args)
{
Debug.Log("这是一个测试输出的方法");
}
}
最后就是触发事件来触发所有执行事件:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MainTest : MonoBehaviour
{
//触发事件鼠标点击事件
private void OnMouseDown()
{
EvenManager.Instance.DispatchEvent(RunTest.EVENT_ONCOLICK, this);
}
}
这样就实现了事件管理器的使用,快动手实现自己的事件管理器吧
总结
事件管理器其实可以认为是一个事件事件管理功能的单例类,通过这个事件管理器,就可以牵线搭桥,连接触发事件与执行事件。