当前位置:   article > 正文

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

unity 观察者模式

目录

前言

一、观察者模式简介

二、效果展示

三、实现步骤

1、事件管理器

2、主播 

3、观众与超管 

4、效果

四、改进

1、定义字典 

2、无参数事件触发 

3、有参数事件触发

最后


前言

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


一、观察者模式简介

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

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

二、效果展示

       

三、实现步骤

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

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

1、事件管理器

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

  1. public class MyEventManager : SingletonBase<MyEventManager>
  2. {
  3. public event UnityAction OnAnchorSmoke;
  4. /// <summary>
  5. /// 为主播吸烟事件添加观察者
  6. /// </summary>
  7. /// <param name="action"></param>
  8. public void AddListener(UnityAction action)
  9. {
  10. OnAnchorSmoke += action;
  11. }
  12. /// <summary>
  13. /// 为主播吸烟事件移出观察者
  14. /// </summary>
  15. /// <param name="action"></param>
  16. public void RemoveListener(UnityAction action)
  17. {
  18. OnAnchorSmoke -= action;
  19. }
  20. /// <summary>
  21. /// 主播吸烟事件触发
  22. /// </summary>
  23. public void TriggerEvent()
  24. {
  25. OnAnchorSmoke?.Invoke();
  26. }
  27. }

2、主播 

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

  1. public class Anchor : MonoBehaviour
  2. {
  3. void Start()
  4. {
  5. }
  6. private void Update()
  7. {
  8. if (Input.GetKeyDown(KeyCode.S))
  9. {
  10. Smoke();
  11. }
  12. }
  13. public void Smoke()
  14. {
  15. Debug.Log($"主播{gameObject.name} 抽了一口烟");
  16. MyEventManager.Instance.TriggerEvent();
  17. }
  18. }

3、观众与超管 

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

观众

  1. public class Viewer : MonoBehaviour
  2. {
  3. private void OnEnable()
  4. {
  5. MyEventManager.Instance.AddListener(Report);
  6. }
  7. private void OnDisable()
  8. {
  9. MyEventManager.Instance.RemoveListener(Report);
  10. }
  11. public void Report()
  12. {
  13. Debug.Log($"观众{gameObject.name} 反手一个举报");
  14. }
  15. }

超管

  1. public class SuperAdministrator : MonoBehaviour
  2. {
  3. private void OnEnable()
  4. {
  5. MyEventManager.Instance.AddListener(Ban);
  6. }
  7. private void OnDisable()
  8. {
  9. MyEventManager.Instance.RemoveListener(Ban);
  10. }
  11. public void Ban()
  12. {
  13. Debug.Log($"超管{gameObject.name} 封禁了主播");
  14. }
  15. }

4、效果

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

四、改进

1、定义字典 

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

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

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

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

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

  1. public class EventArgs
  2. {
  3. public static readonly EventArgs Empty;
  4. public EventArgs();
  5. }

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

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

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

  1. /// <summary>
  2. /// 事件管理器
  3. /// </summary>
  4. public class EventManager : SingletonBase<EventManager>
  5. {
  6. private Dictionary<string, EventHandler> handlerDic = new Dictionary<string, EventHandler>();
  7. /// <summary>
  8. /// 添加一个事件的监听者
  9. /// </summary>
  10. /// <param name="eventName">事件名</param>
  11. /// <param name="handler">事件处理函数</param>
  12. public void AddListener(string eventName, EventHandler handler)
  13. {
  14. if (handlerDic.ContainsKey(eventName))
  15. handlerDic[eventName] += handler;
  16. else
  17. handlerDic.Add(eventName, handler);
  18. }
  19. /// <summary>
  20. /// 移除一个事件的监听者
  21. /// </summary>
  22. /// <param name="eventName">事件名</param>
  23. /// <param name="handler">事件处理函数</param>
  24. public void RemoveListener(string eventName, EventHandler handler)
  25. {
  26. if (handlerDic.ContainsKey(eventName))
  27. handlerDic[eventName] -= handler;
  28. }
  29. /// <summary>
  30. /// 触发事件(无参数)
  31. /// </summary>
  32. /// <param name="eventName">事件名</param>
  33. /// <param name="sender">触发源</param>
  34. public void TriggerEvent(string eventName, object sender)
  35. {
  36. if (handlerDic.ContainsKey(eventName))
  37. handlerDic[eventName]?.Invoke(sender, EventArgs.Empty);
  38. }
  39. /// <summary>
  40. /// 触发事件(有参数)
  41. /// </summary>
  42. /// <param name="eventName">事件名</param>
  43. /// <param name="sender">触发源</param>
  44. /// <param name="args">事件参数</param>
  45. public void TriggerEvent(string eventName, object sender, EventArgs args)
  46. {
  47. if (handlerDic.ContainsKey(eventName))
  48. handlerDic[eventName]?.Invoke(sender, args);
  49. }
  50. /// <summary>
  51. /// 清空所有事件
  52. /// </summary>
  53. public void Clear()
  54. {
  55. handlerDic.Clear();
  56. }
  57. }

2、无参数事件触发 

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

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

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

  1. public class Viewer : MonoBehaviour
  2. {
  3. private void OnEnable()
  4. {
  5. EventManager.Instance.AddListener("AnchorSmoke", Report);
  6. }
  7. private void OnDisable()
  8. {
  9. EventManager.Instance.RemoveListener("AnchorSmoke", Report);
  10. }
  11. public void Report(object sender,EventArgs e)
  12. {
  13. Debug.Log($"观众{gameObject.name} 反手一个举报");
  14. }
  15. }

3、有参数事件触发

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

  1. public class PasswordLotteryEventArgs : EventArgs
  2. {
  3. public string password;
  4. public PasswordLotteryEventArgs(string Password)
  5. {
  6. password = Password;
  7. }
  8. }

主播方法如下

  1. public class Anchor : MonoBehaviour
  2. {
  3. void Start()
  4. {
  5. }
  6. private void Update()
  7. {
  8. if (Input.GetKeyDown(KeyCode.S))
  9. {
  10. Smoke();
  11. }
  12. if (Input.GetKeyDown(KeyCode.L))
  13. {
  14. PasswordLottery("说句心里话,主播真的帅到爆炸");
  15. }
  16. }
  17. public void Smoke()
  18. {
  19. Debug.Log($"主播{gameObject.name} 抽了一口烟");
  20. EventManager.Instance.TriggerEvent("AnchorSmoke",this);
  21. }
  22. public void PasswordLottery(string Password)
  23. {
  24. Debug.Log($"主播{gameObject.name} 发起了口令抽奖,抽奖口令是:{Password}");
  25. EventManager.Instance.TriggerEvent("AnchorPasswordLottery", this,new PasswordLotteryEventArgs(Password));
  26. }
  27. }

 观众方法如下

  1. public class Viewer : MonoBehaviour
  2. {
  3. private void OnEnable()
  4. {
  5. EventManager.Instance.AddListener("AnchorSmoke", Report);
  6. EventManager.Instance.AddListener("AnchorPasswordLottery", ParticipateLottery);
  7. }
  8. private void OnDisable()
  9. {
  10. EventManager.Instance.RemoveListener("AnchorSmoke", Report);
  11. EventManager.Instance.RemoveListener("AnchorPasswordLottery", ParticipateLottery);
  12. }
  13. public void Report(object sender,EventArgs e)
  14. {
  15. Debug.Log($"观众{gameObject.name} 反手一个举报");
  16. }
  17. public void ParticipateLottery(object sender, EventArgs e)
  18. {
  19. var data = e as PasswordLotteryEventArgs;
  20. if (data != null)
  21. {
  22. Debug.Log($"观众{gameObject.name} 发送弹幕:{data.password} 。参与了抽奖!");
  23. }
  24. }
  25. }

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

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

  1. public static class EventName
  2. {
  3. public const string AnchorSmoke = nameof(AnchorSmoke);
  4. public const string AnchorPasswordLottery = nameof(AnchorPasswordLottery);
  5. }

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


最后

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

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/865591
推荐阅读
相关标签
  

闽ICP备14008679号