【Unity百宝箱】游戏中的观察者模式
原创 打工人小棋 打工人小棋 2023-04-27 13:35 发表于广东-
Hi,大家好,我是游戏区“bug主”打工人小棋!
今天我想和大家聊一聊游戏中的观察者模式~
近两期视频,都是在为下一期视频做准备,在下期视频中,大家将会看到用户数据存储、以及观察者模式在游戏中的大量应用和实践,希望能让你们彻底学会这些设计思想。
-
• bilibili: Json用户数据存档
-
• bilibili: 观察者模式
介绍
什么是观察者模式?
我给举个简单的例子,当同学们想要第一时间得知小棋更新视频了,最好的办法是什么?
给你们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主们更新时通知给订阅的粉丝。
这样做的好处在于双方的耦合性极低,发送方只需要通知下去,订阅方就一定会收到消息,至于收到消息后怎么做,发送方不清楚也不关心。
将来有更多人关注小棋了,也就是无脑通知就完事了,粉丝们想要点进来一键三连,或者评论转发都可以。(不点赞的不许走)
代码
废话少说,直接上代码。
-
1. 单例
首先,我们的事件中心作为所有事件的管理器,在游戏中是唯一的,因为我们将其设置为单例模式。
单例的介绍和写法网上教程有很多,我这里就举个最简单的写法。
// 事件中心
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
-
1. 响应事件
当事件发生时,事件中心会把消息通知给感兴趣的订阅者。
这里的订阅者可以使用Unity为我们封装的UnityAction,大家可以简单将其理解为代码中的一个个方法。
当事件触发时,就调用这些方法。
每个事件可能有多个订阅者,比如小棋就有不少粉丝订阅。
因此这里我们使用字典存储所有的事件,字典的key对应订阅名,字典的value对应一个订阅列表。
下面我们对订阅列表进行下封装:
// 使用接口,方便拓展有参事件和无参事件
public interface IEventInfo
{
}
// 无参数事件响应
public class EventInfo : IEventInfo
{
public UnityAction actions;
public EventInfo(UnityAction action)
{
actions += action;
}
}
-
1. 框架设计
有了响应事件后,我们就可以搭建起事件中心的整体框架了。
在事件中心里,最重要的两个方法:
-
• 订阅消息
-
• 通知消息
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;
}
}
首先分清两件最关键的事情:
-
1. 谁订阅消息
-
2. 谁触发消息
在上述情境中,不难分析得到:
-
1. 卡片需要订阅消息,当阳光改变时,他需要检测下卡片是否解锁。
-
2. GameManager需要发布消息,当阳光数量改变时,他需要广播这个消息。
经过上述分析后,我们可以得到重构后的代码:
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();
}
}
其他更多平台:
-
• bilibili 打工人小棋
-
• 知乎 打工人小棋
-
• CSDN 打工人小棋
一起加油 :)
Unity百宝箱5 Unity百宝箱 · 目录 上一篇【Unity百宝箱】游戏中的用户数据存档下一篇【泰裤辣 の Unity百宝箱】分享一套简单易用的游戏UI框架 阅读 184
人划线
标签:name,百宝箱,eventDic,观察者,actions,Unity,action,public,EventInfo From: https://www.cnblogs.com/Jimmy104/p/17933502.html