小工具知识网

当前位置: 首页 > 行业知识 > Unity | 观察者模式(发布-订阅模式)

行业知识

Unity | 观察者模式(发布-订阅模式)

2026-11-06 13:06:43 来源:互联网转载

���录

前言

一、观察者模式简介

二、效果展示

三、实现步骤

1、事件管理器

2、主播 

3、观众与超管 

4、效果

四、改进

1、定义字典 

2、无参数事件触发 

3、有参数事件触发

最后


前言

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


一、观察者模式简介

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

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

二、效果展示

       

三、实现步骤

        先简单介绍一下事件的概念:一个类或者对象中的事件发生后会通知订阅了这个事件的其他类、其他对象。别的类、对象在接收到这个通知之后就会纷纷作出他们各自的响应。

        事件非常契合观察者模式,后续的方法也都将用事件的方式来实现。

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

1、事件管理器

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

public class MyEventManager : SingletonBase{    public event UnityAction OnAnchorSmoke;    ///     /// 为主播吸烟事件添加观察者    ///     ///     public void AddListener(UnityAction action)    {        OnAnchorSmoke += action;    }    ///     /// 为主播吸烟事件移出观察者    ///     ///     public void RemoveListener(UnityAction action)    {        OnAnchorSmoke -= action;    }    ///     /// 主播吸烟事件触发    ///     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 handlerDic = new Dictionary();

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

/// /// 事件管理器/// public class EventManager : SingletonBase{    private Dictionary handlerDic = new Dictionary();    ///     /// 添加一个事件的监听者    ///     /// 事件名    /// 事件处理函数    public void AddListener(string eventName, EventHandler handler)    {        if (handlerDic.ContainsKey(eventName))            handlerDic[eventName] += handler;        else            handlerDic.Add(eventName, handler);    }    ///     /// 移除一个事件的监听者    ///     /// 事件名    /// 事件处理函数    public void RemoveListener(string eventName, EventHandler handler)    {        if (handlerDic.ContainsKey(eventName))            handlerDic[eventName] -= handler;    }    ///     /// 触发事件(无参数)    ///     /// 事件名    /// 触发源    public void TriggerEvent(string eventName, object sender)    {        if (handlerDic.ContainsKey(eventName))            handlerDic[eventName]?.Invoke(sender, EventArgs.Empty);    }    ///     /// 触发事件(有参数)    ///     /// 事件名    /// 触发源    /// 事件参数    public void TriggerEvent(string eventName, object sender, EventArgs args)    {        if (handlerDic.ContainsKey(eventName))            handlerDic[eventName]?.Invoke(sender, args);    }    ///     /// 清空所有事件    ///     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知识。若文章内容错误,麻烦指点。

上一篇:快递服务器被盗怎么办?

下一篇:暗星连不上服务器怎么解决?