Unity下落式音游实现——(3)实现观察者模式
前言
本来这一部分是计划放在后面的,但在整理鼓盘敲击判定时优化了原来的部分代码(删掉了一个不必要地函数),顺理成章地出了bug。最后发现是这个函数原会在另一个脚本中用unity自带地消息系统传递(SendMessage),麻烦在于SendMessage传递函数名参数用的是字符串,所以当我回滚代码,查找那个函数的引用时什么也没找到,很麻烦。于是就先将观察者模式提前…
前期准备
观察者模式
有监听者和被监听者两类对象,当某事件出现时,被监听者会对所有监听者进行广播一段信息,当监听者收到信息,发现这信息是自己感兴趣的内容则执行某些操作,若不是则忽略该信息。我们需要实现一个Messenger对象中心,给监听者提供接口函数AddListener以监听某段信息,给被监听者提供Boradcast以广播某段信息。信息以字符串形式传递,在广播时可以传递参数(意味着我们需要使用模板)
C#委托和事件
类似C++中函数对象,能实现一对多调用函数。监听者使用AddListener在Messenger中注册对应委托事件,被监听者使用Boradcast在Messenger中调用对应委托事件
实现过程
原理
class Observer
{
public:
virtual ~Observer() {}
// 收到消息后做点什么
virtual void onNotify(const Entity& entity, Event event) = 0;
};
class Subject
{
private:
// 需要通知的观察者列表
Observer* observers[MAX_OBSERVERS];
int numObservers_;
public:
void addObserver(Observer* observer);
void removeObserver(Observer* observer);
protected:
// 发通知
void notify(const Entity& entity, Event event)
{
for (int i = 0; i < numObservers_; i++)
observers_[i]->onNotify(entity, event);
}
};
优化
为了避免动态分配,可以预先在对象池中分配一系列Observer和Subject节点,
C#实现(基于消息)
using System;
using System.Collections.Generic;
using System.Linq;
static public class Messenger
{
// 存储已注册的委托事件
readonly public static Dictionary<string, Delegate> eventTable = new Dictionary<string, Delegate>();
static public void AddListener(string eventType, Delegate callback)
{
// 检查委托表中是否含有对应key
if (!eventTable.ContainsKey(eventType))
{
eventTable.Add(eventType, null);
}
// 添加新委托
eventTable[eventType] = Delegate.Combine(eventTable[eventType], callback);
}
static public void RemoveListener(string eventType, Delegate handler)
{
if (!eventTable.ContainsKey(eventType))
return;
eventTable[eventType] = Delegate.Remove(eventTable[eventType], handler);
// 若该信息无人感兴趣,就移除
if(eventTable[eventType] == null)
eventTable.Remove(eventType);
}
static public void Broadcast(string eventType)
{
Delegate d;
// 获得对应委托
if (!eventTable.TryGetValue(eventType, out d))
return ;
// 转化为委托数组
Action[] invocationList = d.GetInvocationList().Cast<T>().ToArray();
foreach (var callback in invocationList)
callback.Invoke();
}
}
参考了一个外国社区上的实现,我只实现了基本功能,没有适配多参数的模板,异常检测啥的也没做,这里贴一下大佬的异常检测
// 简单举例
static public BroadcastException CreateBroadcastSignatureException(string eventType)
{
return new BroadcastException(string.Format("Broadcasting message {0} but listeners have a different signature than the broadcaster.", eventType));
}
public class BroadcastException : Exception
{
public BroadcastException(string msg)
: base(msg)
{
}
}
我们可以在GameEvent里记录事件信息
// 处理广播事件
public static class GameEvent
{
public const string DRUM_HIT = "DRUM_HIT";
public const string DIFFICULTY_CHANGED = "DIFFICULTY_CHANGED";
public const string STATUS_CHANGED = "STATUS_CHANGED";
}
使用
void Awake()
{
Messenger<float>.AddListener(GameEvent.DIFFICULTY_CHANGED, changeSpeed);
}
void OnDestroy()
{
Messenger<float>.RemoveListener(GameEvent.DIFFICULTY_CHANGED, changeSpeed);
}
总结
我们基于C#委托实现了观察者模式,在后续的UI和输入中会频繁地用到。另外监听者调用的函数由于类型是委托,不是字符串,可以正常地找到引用,不用再担心发生前言中的问题
另外Unity自带的SendMessage这么拉,是不是应该完全不用呢?个人感觉也不是,比如下面这段代码中需要调用三个参数不同的函数,就需要挂三个Listener,很麻烦(当然如果参数一样当然还是可以挂Listener,将三个函数封装一下就好)
// 将对应信息传给slider
slider.SendMessage("setStatus", status);
slider.SendMessage("setTarget", tmp);
slider.SendMessage("setMovingTime", movingTime);
如果迫不得已要用SendMessage,请务必要在调用处和被调用的函数处都写上注释表明来源或去处!