赞
踩
今天我想和大家聊一聊游戏中的观察者模式~
近两期视频,都是在为下一期视频做准备,在下期视频中,大家将会看到用户数据存储、以及观察者模式在游戏中的大量应用和实践,希望能让你们彻底学会这些设计思想。
什么是观察者模式?
我给举个简单的例子,当同学们想要第一时间得知小棋更新视频了,最好的办法是什么?
给你们5秒钟思考:5 4 3 2 1
最好的办法当然是点个大大的关注,最好是特别关注,因为这样B站会在第一时间把我更新了的消息通知到你们。
有的同学说,我偏不,我就要守着你的主页,每隔1秒钟刷新下页面,我就不信有人比我快!
这样做,不是说不行,甚至在编程领域,还经常这么做。
比如我之前在 8. 阳光拾取 + 僵尸生成 这期视频中,我们在Update函数中,每一帧都去查看阳光数量是否满足需求,满足时才解锁卡片。
public class Card { public int useSun = 25; void Update() { // 每一帧都检查:太阳数量是否足够 if (GameManager.instance.sunNum >= useSun) { darkBg.SetActive(false); } else { darkBg.SetActive(true); } } } public class GameManager { // 当前阳光数据量 public int sunNum = 0; // 增加阳光 public void AddSunNum(int value) { sunNum += value; } }
这样做同样满足我们的需求,也圆满完成了当时任务。
但是大家想想,如果是一个特大型项目,无数个update 都在执行,那将会是一笔巨大的性能开销。
而且数据发生变化时,往往只有一瞬间,比如小棋发了新视频,这个消息的量级是很小的,而你却需要二十四小时全天候守护,这未免付出太大了。
如果你真的这么做,你真的,我哭死。o(╥﹏╥)o
大家可以把B站的这种特别关注,理解成观察者模式的实现。
他包含两大基本要素:订阅和通知。
粉丝们通过关注提前订阅Up主的更新消息,B站在Up主们更新时通知给订阅的粉丝。
这样做的好处在于双方的耦合性极低,发送方只需要通知下去,订阅方就一定会收到消息,至于收到消息后怎么做,发送方不清楚也不关心。
将来有更多人关注小棋了,也就是无脑通知就完事了,粉丝们想要点进来一键三连,或者评论转发都可以。(不点赞的不许走)
废话少说,直接上代码。
首先,我们的事件中心作为所有事件的管理器,在游戏中是唯一的,因为我们将其设置为单例模式。
单例的介绍和写法网上教程有很多,我这里就举个最简单的写法。
// 事件中心 public class EventCenter { private static EventCenter instance; public static EventCenter Instance { get { if(instance == null) { instance = new EventCenter(); } return instance; } } public void test() { print("test test test") } }
使用时只需要这么写:
EventCenter().Instance.test()
即可看到运行结果:
输出: test test test
当事件发生时,事件中心会把消息通知给感兴趣的订阅者。
这里的订阅者可以使用Unity为我们封装的UnityAction,大家可以简单将其理解为代码中的一个个方法。
当事件触发时,就调用这些方法。
每个事件可能有多个订阅者,比如小棋就有不少粉丝订阅。
因此这里我们使用字典存储所有的事件,字典的key对应订阅名,字典的value对应一个订阅列表。
下面我们对订阅列表进行下封装:
// 使用接口,方便拓展有参事件和无参事件
public interface IEventInfo
{
}
// 无参数事件响应
public class EventInfo : IEventInfo
{
public UnityAction actions;
public EventInfo(UnityAction action)
{
actions += action;
}
}
有了响应事件后,我们就可以搭建起事件中心的整体框架了。
在事件中心里,最重要的两个方法:
public class EventCenter { // 存储所有事件 private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>(); // 订阅消息 public void AddEventListener(string name, UnityAction action) { if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo).actions += action; else _eventDic.Add(name, new EventInfo(action)); } // 通知消息 public void EventTrigger(string name) { if (_eventDic.ContainsKey(name)) if ((_eventDic[name] as EventInfo).actions != null) (_eventDic[name] as EventInfo).actions.Invoke(); } }
在某些时候,我们还需要对无用的消息进行清除,比如取消关注。
// 移除无参数事件的监听
public void RemoveEventListener(string name, UnityAction action)
{
if (_eventDic.ContainsKey(name))
(_eventDic[name] as EventInfo).actions -= action;
}
// 清空事件监听
public void Clear()
{
_eventDic.Clear();
}
以上所提的方法都是无参方法,相当于up主更新视频后,B站只通知粉丝:
小棋更新视频啦~!
但是更新了什么视频却不得而知。
想要在通知的消息里得到更新的视频内容,还需要添加有参方法,在通知时把视频信息一起发送过去。
这里用到了C#语法中的泛型,具体逻辑和无参方法几乎一样。
// 带参数事件响应 public class EventInfo<T> : IEventInfo { public UnityAction<T> actions; public EventInfo(UnityAction<T> action) { actions += action; } } public class EventCenter { // 数据结构 private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>(); // 添加带参数事件的监听 public void AddEventListener<T>(string name, UnityAction<T> action) { // 旧事件 if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo<T>).actions += action; // 新事件 else _eventDic.Add(name, new EventInfo<T>(action)); } // 移除带参数事件的监听 public void RemoveEventListener<T>(string name, UnityAction<T> action) { if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo<T>).actions -= action; } // 分发带参数的事件 public void EventTrigger<T>(string name, T info) { if (_eventDic.ContainsKey(name)) if ((_eventDic[name] as EventInfo<T>).actions != null) (_eventDic[name] as EventInfo<T>).actions.Invoke(info); } }
还是以刚刚卡片的这个逻辑来举例,我们一起思考下:如何以观察者模式重构这段代码。
public class Card { public int useSun = 25; void Update() { // 每一帧都检查:太阳数量是否足够 if (GameManager.instance.sunNum >= useSun) { darkBg.SetActive(false); } else { darkBg.SetActive(true); } } } public class GameManager { // 当前阳光数据量 public int sunNum = 0; // 增加阳光 public void AddSunNum(int value) { sunNum += value; } }
首先分清两件最关键的事情:
在上述情境中,不难分析得到:
经过上述分析后,我们可以得到重构后的代码:
public class Card { public int useSun = 25; void Start() { // Start中添加消息监听,消息命名为: EventSunNumChange EventCenter.Instance.AddEventListener<int>("EventSunNumChange", CheckUnLock); } public void CheckUnLock(int num) { print(">>>>> Listen EventSunNumChange"); // 只在阳光改变时检查:太阳数量是否足够 if (num >= useSun) { darkBg.SetActive(false); } else { darkBg.SetActive(true); } } } public class GameManager { public int sunNum = 0; // 当阳光数量发生改变时,触发消息通知 public void AddSunNum(int value) { sunNum += value; EventCenter.Instance.EventTrigger<int>("EventSunNumChange", sunNum); print(">>>>> Trigger EventSunNumChange"); } }
最后来测试下效果,在植物大战僵尸游戏中:拾取阳光会增加阳光的数量,而卡片需要阳光数量满足条件时才会解锁。
经过验证,消息得到了正确的触发和响应。
代码中的Log也打印出来了:
>>>>> Trigger EventSunNumChange
>>>>> Listen EventSunNumChange
最后把框架的主体代码贴出来:
using System.Collections.Generic; using UnityEngine.Events; // 事件响应空接口,用于支持可有可无的参数类型 public interface IEventInfo { } // 带参数事件响应 public class EventInfo<T> : IEventInfo { public UnityAction<T> actions; public EventInfo(UnityAction<T> action) { actions += action; } } // 无参数事件响应 public class EventInfo : IEventInfo { public UnityAction actions; public EventInfo(UnityAction action) { actions += action; } } // 事件中心 // 负责注册(监听)事件、分发(触发)事件 // 事件支持 带参数 和 无参数 两种 // 带参数事件使用 EventInfo<T> 数据类型 public class EventCenter { // 数据结构 private Dictionary<string, IEventInfo> _eventDic = new Dictionary<string, IEventInfo>(); private static EventCenter instance; public static EventCenter Instance { get { if(instance == null) { instance = new EventCenter(); } return instance; } } // 添加带参数事件的监听 public void AddEventListener<T>(string name, UnityAction<T> action) { // 旧事件 if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo<T>).actions += action; // 新事件 else _eventDic.Add(name, new EventInfo<T>(action)); } // 添加无参数事件的监听 public void AddEventListener(string name, UnityAction action) { if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo).actions += action; else _eventDic.Add(name, new EventInfo(action)); } // 移除带参数事件的监听 public void RemoveEventListener<T>(string name, UnityAction<T> action) { if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo<T>).actions -= action; } // 移除无参数事件的监听 public void RemoveEventListener(string name, UnityAction action) { if (_eventDic.ContainsKey(name)) (_eventDic[name] as EventInfo).actions -= action; } // 分发带参数的事件 public void EventTrigger<T>(string name, T info) { if (_eventDic.ContainsKey(name)) if ((_eventDic[name] as EventInfo<T>).actions != null) (_eventDic[name] as EventInfo<T>).actions.Invoke(info); } // 分发无参数的事件 public void EventTrigger(string name) { if (_eventDic.ContainsKey(name)) if ((_eventDic[name] as EventInfo).actions != null) (_eventDic[name] as EventInfo).actions.Invoke(); } // 清空事件监听 // 主要用于场景切换时防止内存泄漏 public void Clear() { _eventDic.Clear(); } }
更多学习资料欢迎关注公众号:打工人小棋
其他更多平台:
一起加油 :)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。