Unity | 观察者模式(发布-订阅模式)
观察者模式是软件开发中一种十分常见的设计模式,又被称为发布-订阅模式,属于行为型模式的一种。它定义了一种一对多的依赖关系,让多个观察者对象(Observer)同时监听某一个主题对象(Subject)。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
目录
前言
仅个人学习的记录,旨在分享我的学习笔记和个人见解。
一、观察者模式简介
观察者模式是软件开发中一种十分常见的设计模式,又被称为发布-订阅模式,属于行为型模式的一种。它定义了一种一对多的依赖关系,让多个观察者对象(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知识。若文章内容错误,麻烦指点。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)