目录

前言

一、观察者模式简介

二、效果展示

三、实现步骤

1、事件管理器

2、主播 

3、观众与超管 

4、效果

四、改进

1、定义字典 

2、无参数事件触发 

3、有参数事件触发

最后


前言

        仅个人学习的记录,旨在分享我的学习笔记和个人见解。


一、观察者模式简介

        观察者模式是软件开发中一种十分常见的设计模式,又被称为发布-订阅模式,属于行为型模式的一种。它定义了一种一对多的依赖关系,让多个观察者对象(Observer)同时监听某一个主题对象(Subject)。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

        以网络直播为例,在场景中有:主播、超管、观众三个角色,主播作为被观察者(主题对象),超管与观众作为观察者时刻关注主播(主题对象)的操作,当主播违规抽烟时(主题对象状态改变),观众与超管同时作出响应(观察者状态更新),观众纷纷起哄并点起了举报,超管封禁了主播72小时。

二、效果展示

       

三、实现步骤

        先简单介绍一下事件的概念:一个类或者对象中的事件发生后会通知订阅了这个事件的其他类、其他对象。别的类、对象在接收到这个通知之后就会纷纷作出他们各自的响应
        事件非常契合观察者模式,后续的方法也都将用事件的方式来实现。

        将 主播吸烟 作为一个事件,当主播吸烟触发后,订阅主播的超管与观众纷纷作出响应。

1、事件管理器

       在事件管理器中,定义了一个 主播吸烟的事件,在外部,观众和超管可以通过调用AddListener方法来订阅 主播吸烟 事件。 而主播在吸烟后会调用TriggerEvent方法,让观察者们作出各自的响应。

public class MyEventManager : SingletonBase<MyEventManager>
{
    public event UnityAction OnAnchorSmoke;
    /// <summary>
    /// 为主播吸烟事件添加观察者
    /// </summary>
    /// <param name="action"></param>
    public void AddListener(UnityAction action)
    {
        OnAnchorSmoke += action;
    }
    /// <summary>
    /// 为主播吸烟事件移出观察者
    /// </summary>
    /// <param name="action"></param>
    public void RemoveListener(UnityAction action)
    {
        OnAnchorSmoke -= action;
    }
    /// <summary>
    /// 主播吸烟事件触发
    /// </summary>
    public void TriggerEvent()
    {
        OnAnchorSmoke?.Invoke();
    }
}

2、主播 

当按下键盘S键的时候,代表主播抽烟。

public class Anchor : MonoBehaviour
{
    void Start()
    {
        
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            Smoke();
        }
    }
    public void Smoke()
    {
        Debug.Log($"主播{gameObject.name} 抽了一口烟");
        MyEventManager.Instance.TriggerEvent();
    }
}

3、观众与超管 

为了简单方便演示,所有动作仅用Debug代替。
值得注意的是:在订阅事件后需要再合适的地方取消订阅,不然即使是销毁了观察者物体,但事件管理器中仍然有观察者响应方法的引用,导致脚本对象无法从内存中释放,出现内存占用问题。
一般在Start/OnEnble中订阅,在OnDestroy/OnDisable中取消订阅。

观众

public class Viewer : MonoBehaviour
{
    private void OnEnable()
    {
        MyEventManager.Instance.AddListener(Report);
    }

    private void OnDisable()
    {
        MyEventManager.Instance.RemoveListener(Report);
    }
    public void Report()
    {
        Debug.Log($"观众{gameObject.name} 反手一个举报");
    }
}

超管

public class SuperAdministrator : MonoBehaviour
{
    private void OnEnable()
    {
        MyEventManager.Instance.AddListener(Ban);
    }

    private void OnDisable()
    {
        MyEventManager.Instance.RemoveListener(Ban);
    }
    public void Ban()
    {
        Debug.Log($"超管{gameObject.name} 封禁了主播");
    }
}

4、效果

把脚本挂载在各自物体上后开始运行,按下S后,观察者作出各自响应

四、改进

1、定义字典 

由于在整个项目中有许多需要用到观察者模式的地方,所以在事件管理器中仅声明1个事件并不能满足需求,以上述主播观众场景为例,再添加 口令抽奖等事件,其中口令抽奖需要一个string的参数作为口令。

众多的事件处理器可以用字典来存储,不同事件所需要的不同参数可以用EventArgs来传递。

C#提供了一个通用的委托:EventHandler
它的结构是这样的,其中sender为事件源,e则为传递的参数。

public delegate void EventHandler(object sender, EventArgs e);

EventArgs的结构是这样的,Empty为只读,里边没有能存储参数的地方,所以如果需要传递参数的话需要新建一个类并继承EventArgs,自己定义成员变量来存储参数。

    public class EventArgs
    {
        public static readonly EventArgs Empty;
        public EventArgs();
    }

 所以,改进后新建的字典的key为事件名,value为EventHandler。

private Dictionary<string, EventHandler> handlerDic = new Dictionary<string, EventHandler>();

对于字典,我们需要给他定义的操作有:添加事件处理器、移出事件处理器、触发特定事件、清空事件。

/// <summary>
/// 事件管理器
/// </summary>
public class EventManager : SingletonBase<EventManager>
{
    private Dictionary<string, EventHandler> handlerDic = new Dictionary<string, EventHandler>();
    /// <summary>
    /// 添加一个事件的监听者
    /// </summary>
    /// <param name="eventName">事件名</param>
    /// <param name="handler">事件处理函数</param>
    public void AddListener(string eventName, EventHandler handler)
    {
        if (handlerDic.ContainsKey(eventName))
            handlerDic[eventName] += handler;
        else
            handlerDic.Add(eventName, handler);
    }
    /// <summary>
    /// 移除一个事件的监听者
    /// </summary>
    /// <param name="eventName">事件名</param>
    /// <param name="handler">事件处理函数</param>
    public void RemoveListener(string eventName, EventHandler handler)
    {
        if (handlerDic.ContainsKey(eventName))
            handlerDic[eventName] -= handler;
    }
    /// <summary>
    /// 触发事件(无参数)
    /// </summary>
    /// <param name="eventName">事件名</param>
    /// <param name="sender">触发源</param>
    public void TriggerEvent(string eventName, object sender)
    {
        if (handlerDic.ContainsKey(eventName))
            handlerDic[eventName]?.Invoke(sender, EventArgs.Empty);
    }
    /// <summary>
    /// 触发事件(有参数)
    /// </summary>
    /// <param name="eventName">事件名</param>
    /// <param name="sender">触发源</param>
    /// <param name="args">事件参数</param>
    public void TriggerEvent(string eventName, object sender, EventArgs args)
    {
        if (handlerDic.ContainsKey(eventName))
            handlerDic[eventName]?.Invoke(sender, args);
    }
    /// <summary>
    /// 清空所有事件
    /// </summary>
    public void Clear()
    {
        handlerDic.Clear();
    }
}

2、无参数事件触发 

现在, 在主播脚本中触发“抽烟”事件的代码需要改成这样,用“AnchorSmoke”来指明触发的具体事件:

public void Smoke()
    {
        Debug.Log($"主播{gameObject.name} 抽了一口烟");
        EventManager.Instance.TriggerEvent("AnchorSmoke",this);
    }

订阅“抽烟”事件的代码则改成这样:

public class Viewer : MonoBehaviour
{
    private void OnEnable()
    {
        EventManager.Instance.AddListener("AnchorSmoke", Report);
    }

    private void OnDisable()
    {
        EventManager.Instance.RemoveListener("AnchorSmoke", Report);
    }
    public void Report(object sender,EventArgs e)
    {
        Debug.Log($"观众{gameObject.name} 反手一个举报");
    }

}

3、有参数事件触发

当事件需要带有参数时,需要定义一个类继承EventArgs,如只传递一个string参数的参数类:

public class PasswordLotteryEventArgs : EventArgs
{
    public string password;
    public PasswordLotteryEventArgs(string Password)
    {
        password = Password;
    }
}

主播方法如下

public class Anchor : MonoBehaviour
{
    void Start()
    {
        
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            Smoke();
        }
        if (Input.GetKeyDown(KeyCode.L))
        {
            PasswordLottery("说句心里话,主播真的帅到爆炸");
        }
    }
    public void Smoke()
    {
        Debug.Log($"主播{gameObject.name} 抽了一口烟");
        EventManager.Instance.TriggerEvent("AnchorSmoke",this);
    }
    public void PasswordLottery(string Password)
    {
        Debug.Log($"主播{gameObject.name} 发起了口令抽奖,抽奖口令是:{Password}");
        EventManager.Instance.TriggerEvent("AnchorPasswordLottery", this,new PasswordLotteryEventArgs(Password));
    }
}

 观众方法如下

public class Viewer : MonoBehaviour
{
    private void OnEnable()
    {
        EventManager.Instance.AddListener("AnchorSmoke", Report);
        EventManager.Instance.AddListener("AnchorPasswordLottery", ParticipateLottery);
    }

    private void OnDisable()
    {
        EventManager.Instance.RemoveListener("AnchorSmoke", Report);
        EventManager.Instance.RemoveListener("AnchorPasswordLottery", ParticipateLottery);
    }
    public void Report(object sender,EventArgs e)
    {
        Debug.Log($"观众{gameObject.name} 反手一个举报");
    }

    public void ParticipateLottery(object sender, EventArgs e)
    {
        var data = e as PasswordLotteryEventArgs;
        if (data != null)
        {
            Debug.Log($"观众{gameObject.name} 发送弹幕:{data.password} 。参与了抽奖!");
        }
        
    }
}

 现在再次运行,按下L主播发起抽奖,观众响应。 按下S主播抽奖,观众与超管响应。

到此,一个较为实用的观察者模式已经实现,不过其中仍然有可以优化的地方,如代码中的事件名,每次都需要手打,而且在打错之后不易发现错误。所以可以定义一个全局的脚本,用于存放这些事件的名字:

public static class EventName
{
    public const string AnchorSmoke = nameof(AnchorSmoke);
    public const string AnchorPasswordLottery = nameof(AnchorPasswordLottery);
}

 在其他脚本中再写事件名,只需要这样写就行了:


最后

        文章内容仅为个人学习记录。好记性不如烂笔头,为了能更好的回顾和总结,开始记录与分享自己学到的Unity知识。若文章内容错误,麻烦指点。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐