前言
当我在学习unity一段时间后,由于自己那不自量力的野心膨胀,总是想要做一个较大体量的游戏出来。于是我奋指疾敲,想要在Visual Studio的黑框框和五彩斑斓的变量名里敲出一个庞大的游戏世界。但是很快我败下阵来:
①这十几个脚本咋管理嘛!好混乱啊!哎呀,这里看来必须要引用那个脚本了,设置成public在编辑器里拖一拖吧,一段时间后:尼玛怎么拖了这么多。②我靠,这个脚本里代码四五百行,改动的话需要考虑很多多,复杂度成倍上升。③要不要动这个脚本啊?可是它里面关系到十几个脚本啊,我“感动”嘛,根本不敢动啊!
诸如此类的的问题都非常棘手,我意识到,游戏要稍微做出点体量,必须解决它们!可是该怎么解决啊?我上B站大学搜来搜去,搞来搞去,最后终于知道了解决代码结构之间问题的学问叫做“编程设计模式”。又过了一段时间,我了解到了设计模式里的“观察者模式”,学了一段时间后,我感慨道:观察者模式,你是我的神!
观察者模式在我后来的编程中,起到的作用好比纸巾和拉屎之间的关系,可以说是作用巨大,根本离不开。那么接下来,我就按照我自己的理解方式,还有我在实践中的具体运用,来谈谈“观察者模式”吧。
啥是“观察者模式”?
观察者模式(有时又被称为模型(Model)-视图(View)模式、源-收听者(Listener)模式或从属者模式)是软件设计模式的一种。在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。 ——百度
好,百度已经给我们解释完了,这一块咱们pass掉,接着往下走。
哈哈,开玩笑。这解释得也太抽象了吧,没关系,我来用更简单的方式向你解释这一概念。
首先我们要知道观察者模式中的两种角色:【观察者】和【被观察者】
想象有一个人气明星,比如周杰伦~。杰伦有非常多的歌迷,这些歌迷对他的新专辑翘首以盼(话说距离上次伟作已经过去两年了……),都在关注着杰伦的专辑动向,一有点风吹草动大家就会沸腾。那么,观察杰伦的歌迷们就是【观察者】,被歌迷们观察的杰伦就是【被观察者】。
在观察者模式中,被观察者的状态发生改变时,就会向所有的观察者们发送通知,观察者们就可以根据这个通知做出各自相应的行为。类比到杰伦和歌迷上,就是当杰伦发新专辑时(简直天方夜谭!),他会在各种社交媒体、音乐软件上发布这个消息,而所有关注杰伦的歌迷在看到这一消息后,有的掏出钱包,有的奔走相告,有的因激动而变身狒狒:吼吼哇哇!
当然,观察者模式中还有一个很重要的概念:【主题】,我更习惯称呼它为【中间体】。中间体是观察者和被观察者之间的桥梁,就好像一个代理人,负责管理有哪些观察者正在观察自己代理的被观察者。每当被观察者状态改变发送消息时,消息首先到达中间体,再由中间体传递出去。在杰伦和歌迷的比喻中,中间体就好比是社交媒体、音乐平台。
为什么说“观察者模式”优秀?
观察者模式之优秀,以至于C#官方都将其写入语法之中:委托delegate、事件event。那具体优秀在哪些方面呢?
一、低耦合
在编程中,中间体以一种抽象形式存在,这样使得观察者和被观察者彼此之间不相互依赖,而是单独依赖于中间体这个抽象。 你可能会想,为什么不让观察者和被观察者直接联系起来,这样岂不更加直观?其实,引入一层中间体并不是为了增加复杂性,而是为了增加设计的灵活性和可扩展性。尽管从表面上看,直接让两者联系可能看起来更简单,但这种直接联系实际上会限制系统的灵活性和可维护性:直接联系意味着观察者和被观察者之间将存在紧密的耦合,如果未来需要更改被观察者的实现或添加新的观察者类型,这种紧密耦合将导致大量的代码修改和潜在的错误。
二、支持一对多通信
被观察者会向所有已注册的观察者对象发送通知,这种机制简化了一对多系统设计的难度。当观察目标的状态发生变化时,所有相关的观察者都能自动接收到通知并作出相应的处理。
这使得在游戏中某些一对多事件编程中更加容易,比如“战斗结束”这个事件触发后,可能还要触发一系列的事件比如“玩家血量重置”、“新关卡解锁”、“奖励发放”、“NPC对话”等等。这样的话,运用观察者模式,让“战斗结束”成为被观察者,让诸如“玩家血量重置”等事件成为观察者,这样一旦“战斗结束”的通知发了出来,所有的观察者收到消息并且被触发。
三、易于拓展
- 开闭原则:观察者模式满足开闭原则的要求,即软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。在观察者模式中,增加新的观察者无需修改原有系统代码,只需创建新的观察者类并实现更新接口即可。
- 灵活性:由于观察者和观察目标之间的耦合度低,因此系统可以灵活地添加、删除或修改观察者,而不会对其他部分造成太大影响。
四、 表现层和数据层分离
观察者模式定义了稳定的消息更新传递机制,并抽象了更新接口。这使得可以有各种各样不同的表示层充当具体观察者角色,从而实现了表示层和数据逻辑层的分离。
运用方式之一:事件中心
观察者模式的运用之一就是【事件中心】。
想象有一个失物招领的大厅,很多丢失物品的人在大厅的工作人员处登记:自己丢失了手机、自己私房钱不见了、自己放在户外的雪糕不见了等等,然后就回家去了。过了几天,有人捡到了手机,来到失物招领大厅上交手机(拾金不昧,我辈榜样)。大厅的工作人员查询登记簿后,发现这是某个人丢失的手机,于是通知这个人。丢失手机的人得到消息后,欢天喜地来到大厅处领取几年没见(一日不见,如隔三秋)的手机。
在这个例子中,失物招领大厅相当于事件中心(中间体);在大厅登记自己丢失物品的人们则是观察者,他们通过在大厅注册登记,从而让大厅能够时刻替他们留意自己的东西有没有被别人捡到;捡到手机的人是被观察者,在捡到手机后向大厅发送“自己捡到了手机”这个消息;大厅在得到这个消息后,在查询登记簿后通知丢失了手机的人。
假设在游戏中,我们需要在战斗结束后为玩家发放奖励,那么运用事件中心的思想,我们可以像下面这样做(注意,代码只是展示思路,很多地方并不严谨):
创立一个事件中心:
一个用来发放奖励的对象:
在脚本初始化时,奖励发放者把触发奖励的事件类型(战斗结束)和自己发放奖励的行为注册给事件中心
一个控制战斗结束的对象:
向事件中心广播”战斗结束“事件的对象。
在这些脚本中,一旦BattleManager中的BattleEnd方法被调用,那么便会向事件中心发送”战斗结束“的通知,而RewardGiver先前已经在事件中心注册好了对应的事件和触发行为,所以事件中心会找到这个注册,并且激活这个注册中的行为,也就是奖励发放行为。这样一来,战斗结束后玩家便能收到奖励。
通过事件中心的思想,我们解耦了”控制战斗结束“的脚本和”发放奖励“的脚本,使其只依赖于事件中心这个抽象,两者之间的关联性大大降低。就好比失物招领大厅的例子中,丢失物品的人和捡到物品的人之间,并没有直接的接触,几乎没有什么关联。
运用方式之二:事件类
这种对于观察者模式的运用方式其实原理和事件中心是大差不差的。只不过这种方式将各种【主题】或者叫做【中间体】分散成各种事件类,一个中间体就是一个事件类,并不集中在某一个中心。
还是以上述战斗结束发放奖励为例子,我们运用事件类的思想来实践观察者模式。
首先创造一个事件类的基类,之后所有的事件类都继承自此基类:
然后我们创造一个“战斗结束”类继承自基类:
之后同样的,在RewardGiver里通过“战斗结束”类注册奖励发放行为:
在BattleMManager内战斗结束时通过“战斗结束”类广播事件:
可以看到,其基本代码和事件中心的代码是很像的。用事件类思想的好处是通过查看有多少种事件类,就可以一目了然的知道游戏内的各种事件种类。当不需要事件时,直接删除对应的事件类即可。
结尾
观察者模式是一种非常优秀的思想,天知道前辈们是怎么一步一步总结出来的,真是众多智慧凝聚的产物。
希望我的这篇探讨观察者模式的文章能够帮到在开发游戏的你们,咱们下一篇文章再见!