Unity设计模式—中介者模式
中介者模式:用一个中介对象封装一系列的对象交互,中介者使对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
我见
传统的中介者模式写法个人用的很少,写起来很麻烦,如下图:
而且由于具体中介者类包含了大量同级对象的交互细节,可能会变得很复杂,最终难以维护。
但是中介者模式的思想非常有用,有好多种非常常用的应用场景,每种场景都值得专门写一篇blog。
所以,这一篇blog会重点介绍几种中介者模式的变种(应用),而非实现上图所示的中介者模式。
以下几种使用了中介者模式的方法都非常常用
- 中介者模式和观察者模式结合的发布/订阅系统
- 服务定位器模式(TODO:专题介绍)
- MVP/MVVM模式里的Presenter/ViewModel也是中介者(TODO:专题介绍)
中介者模式和观察者模式结合的发布/订阅系统
需求
试想一个需求:在游戏里有NPC对象和HireView招聘录用界面。
点击招聘录用界面的“录用”按钮,主场景的人播放庆祝动作。
常规做法如下:
1.要么由界面通知npc做出更新,即view依赖npc:
public class HireView(){
public void OnHireButtonClick(string id){
//处理这个界面的逻辑
this.UpdateView();
//通过某种手段获取npc对象
NPC npc = Global_Get_NPC(id);
npc.Happy();
}
}
2.要么由npc监听view的按钮点击事件,即npc依赖view
hireView.btnHire.onClick.AddListener(Happy);
不管是哪种方案,都能实现需求。
但问题都是一样的:NPC和HireView耦合了。
若只是一处地方需要这样倒也罢了,游戏开发中处处可见类似的情况:
- 玩家血量变化了,相应系统要跟随变化
- 角色获得金钱了,相应系统要跟随变化
- 界面里的购买按键点击了,3d场景里要刷出对应的对象,相关界面还要刷新
一般来说,为了达到订阅/刷新(即观察者),我们可以使用Action,UnityAction,Event,UnityEvent的预设的委托,即观察者模式的应用。
但为了让系统代码松耦合,我们要引进一个EventManager(即中介者),用它来处理游戏中的各种事件发布/订阅,即中介者模式的应用。
接下来就介绍EventManager:中介者模式和观察者模式结合的发布/订阅系统。
实现
EventManager是一个单例,是所有观察者事件的中介
public sealed class EventManager
{
private Dictionary<string, Action<object, EventParams>> _eventDict;
// Lazy Initialize instance
private readonly static Lazy<EventManager> _lazy = new Lazy<EventManager>(() => new EventManager());
public static EventManager Instance
{
get
{
return _lazy.Value;
}
}
private EventManager()
{
_eventDict = new Dictionary<string, Action<object, EventParams>>();
}
/// <summary>
/// Subscribe one event
/// </summary>
/// <param name="eventName"> string of the name of event</param>
/// <param name="listener"> delegate of callback mapped to the event </param>
public void Subscribe(string eventName, Action<object, EventParams> listener)
{
Assert.IsNotNull(eventName, "Subscribe eventName should not be null");
// new or add delegate to _eventDict
if (_eventDict.ContainsKey(eventName))
{
_eventDict[eventName] += listener;
}
else
{
_eventDict.Add(eventName, listener);
}
}
/// <summary>
/// Unsubscribe one event
/// </summary>
/// <param name="eventName"> string of the name of event </param>
/// <param name="listener"> delegate of callback mapped to the event </param>
public void UnSubscribe(string eventName, Action<object, EventParams> listener)
{
Assert.IsNotNull(eventName, "UnSubscribe eventName should not be null");
Action<object, EventParams> funcDelegate;
if (_eventDict.TryGetValue(eventName, out funcDelegate))
{
funcDelegate -= listener;
}
}
/// <summary>
/// publish the event
/// </summary>
/// <param name="eventName"> string of the name of event</param>
/// <param name="sender"> object who send the event </param>
/// <param name="eventParams"> EventParams to send </param>
public void Publish(string eventName, object sender, EventParams eventParams)
{
Assert.IsNotNull(eventName, "Publish eventName should not be null");
Action<object, EventParams> funcDelegate;
if (_eventDict.TryGetValue(eventName, out funcDelegate))
{
funcDelegate.Invoke(sender, eventParams);
}
}
}
调用
-
订阅
private void Awake() { EventManager.Instance.Subscribe(EventConst.Hire_NPC, PlayHappy); _animator = this.gameObject.GetComponent<Animator>(); } private void PlayHappy(object sender, EventParams eventParams) { Debug.Log($"npc接收到事件sender={sender},param={eventParams}"); float salary = eventParams.Get<float>("salary"); }
-
发布
EventParams param = new EventParams(); param.Put<float>("salary", UnityEngine.Random.Range(500, 1500)); param.Put<int>("duration", 10); EventManager.Instance.Publish(EventConst.Hire_NPC, this, param);
设计细节
吐槽一下……
C#强类型检查实现回调函数传递真麻烦……委托作为函数指针没问题,但是涉及到传参情况就麻烦多了。
因为有方法重载的存在,不能传一个方法名就完成了。
Subscribe的参数要么用字典,key是string,value是object(方案1)
public void Subscribe(string eventName, Action<object, Dictionary<string,object>> listener)
要么用泛型方法(方案2)
public void Subscribe(string eventName, Action<object, EventParams> listener)
public class EventParams
{
private Dictionary<string, object> keyValuePairs;
public void Put<T>(string name, T val)
{
}
public void Get<T>(string name)
{
}
}
方案1在调用的时候很好写(一行就能搞定)
EventManager.Instance.Publish("Hire", this,new Dictionary<string, object>() { { "salary", 2000 } } );
方案2在调用的时候很难写(一定要拆成多行)
EventParams param = new EventParams();
param.Put<float>("salary", 2000);
EventManager.Instance.Publish("Hire", this, param);
但是!方案1的每个参数一定会设计装箱/拆箱操作,性能上差一点。算了,我还是用方案2了,调用麻烦一点可以接受。
对性能不敏感,用方案1也是可以的,方便使用嘛。
在这一点上不如Lua这样的弱检查类型语言类型方便。(有得必有失啊,原生lua没有方法重载也有没有方法重载的麻烦)
TODO:
Subscribe(string eventName, Action<object, EventParams> listener,object target)
UnSubscribe(object target)
以上两个方法暂未实现。
若要集成到游戏里,最好实现以上两个方法。
在订阅的时候把自己传到EventManager里去,把hashId存下来。
在销毁的时候调用UnSubscribe(object target),取消订阅与自己有关的所有事件。
源码
完整代码已上传至nickpansh/Unity-Design-Pattern | GitHub