ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

观察者模式笔记

观察者模式笔记 观察者模式和事件中心的关系事件中心是观察者模式的中心化类型观察者模式比事件中心更广义不使用事件中心两个类类B监听类A的事件也是观察者模式。观察者模式比调用好在哪监听比调用并没有减少耦合数量只是改变引用方向从主动者引用被动者变成被动者引用主动者。因为人的直观逻辑是原因不必关心结果结果必须关心原因没有因就没有果反映在程序里没有主动者提供的数据被动者不知道怎么处理。监听符合了这个逻辑。这也是为什么监听会有一种“降低耦合”的错觉尽管引用并没有少。适用观察者模式的情景特征首先一定是跨类调用。被调用类较难访问到。比如管理器更新玩家血量要更新HUD显示HUD是面板由UI管理器管理要让UI管理器返回HUD。被调用类生命周期比调用类短。比如一个面板它在关卡里有时不存在管理器直接调用它要判定是不是空。一个操作要if判断在不同情景下要引起不同类的响应时观察者模式可以省去这个if。举例在玩家可以步行、开汽车、开坦克、开飞机的游戏里玩家步行时根本不存在汽车、坦克对象输入模块直接调用还要判定玩家有没有载具还要每一帧判定改成观察者之后玩家开车步行移动方法解除监听汽车移动方法添加监听。观察者更适合一调用多的场景比如情景4比如点击地面多个临时面板要隐藏人物要走过去。而多调一情景比如情景2则可以用判空单例直接调用。适用观察者模式的情景玩家输入的灵活绑定、解绑人物上车WASD解绑人物移动绑定汽车移动音量控制、多语言设置、红点系统声源、文本、按钮散落在软件各处且随时可能出现、销毁音量、语言管理器很难获得所有控件的引用和出现、销毁时机为什么要用事件中心我觉得在Unity里最大的原因还是脚本的生命周期不适合两两的观察者模式。写一个不用事件中心的观察者模式类B监听类A的事件A和B都继承MonoBehaviour马上会发现如果A先于B创建需要在脚本执行顺序里设置脚本优先级那么也会先于B销毁B试图取消监听时A已经销毁。这样还算能用假如现在A也要监听B呢假设A是UIB是管理器A收到输入通知B处理B处理完要通知A刷新显示任意一方先创建它都会找不到自己要监听的对象。那么解决方法就是搞一个生命周期比所有MonoBehaviour都长的对象。直接调用、监听委托、事件中心如何选用看要调用的函数获取到的难度或者说调用链长度。要执行的函数就是此函数的参数的成员方法时直接调用。要执行的函数只在这个类的部分对象要执行毫不犹豫使用观察者模式。比如背包数据类玩家和敌人都有背包只有玩家那个需要触发面板刷新那背包数据类如果引用面板还要判断自己是不是玩家的背包。用事件中心也会导致所有背包更新都通知ui所以适合监听委托。被监听者是很容易访问的管理器类用监听委托。被监听者和监听者都难访问到用事件中心事件中心要满足什么功能支持无参和多种参数的事件最好能避免装箱拆箱能防止重复添加同一个监听方法一个类能发布多种事件也能监听多种事件事件中心实现方案接口集合式和它的问题接口列表式所有观察者继承一个接口被观察者持有接口的HashSet/List分发事件时把集合里的接口方法调用一遍。可以知道List有没有重复添加观察者HashSet更可以直接杜绝重复添加。然后事件分发者可以通过调用事件中心分发也可以继承事件分发者接口……然后我们发现事件分发者都要有一个HashSetIListener有了字段就不能是接口了只能是抽象类那么分发事件的类就不能继承其他类了……所以还是用事件中心。然后又发现一个对象可能要监听多个事件有多个响应函数继承接口、实现方法只有一个响应函数事件中心实现方案多播委托式多播委托式观察者方法在里面通过链表管理通过GetInvocationList().Contains(action)防止重复添加。事件中心实现方案HashSet委托式和它的问题我们决定使用HashSet委托装载监听者。HashSet避免了重复注册委托保证一个监听者可以用自己的多个函数监听多个事件。但是还要处理有参数的情景。想到用各种参数类型委托的基类HashSet装就用HashSetDelegate.using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; namespace MyFarm { public class MyEventCenter2:MySingletonMyEventCenter2 { Dictionarystring, HashSetDelegate eventDic new(); public void Add(string eventName, UnityAction action) { if (!eventDic.ContainsKey(eventName)) { eventDic[eventName] new HashSetDelegate(); } eventDic[eventName].Add(action); } public void Remove(string eventName, UnityAction action) { if (eventDic.ContainsKey(eventName)) { eventDic[eventName].Remove(action); } } public void Trigger(string eventName) { if (eventDic.ContainsKey(eventName)) { foreach (var del in eventDic[eventName]) { if (del is UnityAction action) { action?.Invoke(); } } } } public void AddT(string eventName, UnityActionT action) { if (!eventDic.ContainsKey(eventName)) { eventDic[eventName] new HashSetDelegate(); } eventDic[eventName].Add(action); } public void RemoveT(string eventName, UnityActionT action) { if (eventDic.ContainsKey(eventName)) { eventDic[eventName].Remove(action); } } public void TriggerT(string eventName,T t) { if (eventDic.ContainsKey(eventName)) { foreach (var del in eventDic[eventName]) { if (del is UnityActionT action) { action?.Invoke(t); } } } } } }这里支持了无参和一个参数。多个参数用结构体或ValueTuple包装即可。支持同一个事件名多种不同参数类型触发的时候is会过滤出和Trigger参数类型相同的委托执行。事件的键用字符串常量还是枚举一开始我感觉都一样但是事件达到几十个上百个后字符串常量可能会定义到之前已有的字符串如果常量名不一样但是常量值一样。枚举没有这个问题。但是如果保持常量值和常量名一样且定义在一个类也能自动检查出重名。事件名多了后可能和事件中心单例的首字母一样造成自动补全不变。但是枚举有一个致命问题是对于动态配置的没有事先登记的事件名只能写数字事件意义不明还极易重名。此问题主要出在任务系统。可以考虑任务系统另用一个事件中心使用字符串。Unity中如何保证添加监听时发布者一定存在这就是Awake、Start的用途之一Awake里保证单例存在Start里添加监听和脚本执行顺序无关。如果是非MonoBehavior成员则除了构造函数再弄一个Init在Init里监听别人在Awake里new构造Start里Init。总之就是双初始化机制。但是销毁时没有全部脚本执行一个生命周期函数后执行下一个导致我们现在都无法保证在OnDisable、OnDestroy里解除监听时发布者还存在。事件触发时企图修改委托集合为了防止重复添加我们使用了HashSetDelegate存放委托。现在我们做任务系统发布事件任务完成然后马上移除监听就在foreach循环里修改了集合导致报错。如果用多播委托不方便防止重复添加但没有这个问题。那么触发前把HashSet生成一个数组遍历触发。
返回列表