Unity设计模式—中介者模式

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

其他设计模式

专题 | Unity3D游戏开发中的设计模式 | 问渠 (wenqu.site)

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NickPansh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值