一个通用游戏后台的设计模式实践总结

搞业务开发的时候,发现有一些代码的开发会让人感觉非常简便舒服,有一些代码的开发却有时候会让人感觉心智负担比较大。

逐步总结的过程中,发现让开发人员写起来感觉舒服的代码,大概率是因为当前模块与其他模块代码耦合度低,开发人员无需花费过多的精力去关注其他模块的实现,只需要专注于当前自己的功能实现即可。

而通过实地对多个项目代码设计的对比,发现都有一个非常通用的设计模式,后面再通过阅读一些经验分享文章中,发现大部分后台项目也都运用这个模式,充分实现了模块之间的解耦,极大提高了开发人员的幸福感,这个通用的设计模式便是:发布订阅模式(游戏后台通常也叫事件监听模式 or 事件分发模式)。

一、什么是发布订阅(事件监听)设计模式

发布订阅模式是一个简单通用的模式,一般来说会专门实现一个类如LogicEventDispatch的类来负责维护发布者与订阅者之间关系,也就是下图红框的位置。

发布者有事件需要抛出时,只需要把事件传给LogicEventDispatch,再由LogicEventDispatch去调用各个订阅者的OnEventUpdate函数。以此来完成一次事件的发布与通知的整个流程。

此处参考:观察者模式与订阅发布模式的区别 - 一像素 - 博客园

发布订阅模式

上面这种实现只是一种通用实现之一。为何发布订阅模式可以充分解耦模块之间的耦合呢?因为各个模块只需要关注自己需要的事件,不需要关注各个模块具体实现是什么,开发者只需要知道只要有事件抛出他就处理,其他事情与现有功能开发无关,因此各个模块实现更加内聚,责任更加清晰,由各个模块负责人各自保证自己的代码质量,尽量减少多人同时修改同一个模块的行为。

二、为何需要发布订阅(事件监听)设计模式

发布订阅(事件监听)设计模式其最大功能便是实现模块之间的解耦。解耦的意义大家的都知道好,但是好在哪呢?这就要从项目研发状况说起:

游戏需求变更快,开发量大,特别是在项目临近测试节点时,策划同学经常性会有临时新增需求(临近测试,基本各个模块功能测试同学都会逐一验证,会经常性发现有功能遗漏),这种需求一般排期都是比较紧张,开发人员很难同时兼顾开发效率与开发质量。为了开发效率,开发人员通常来说很难去完全通读原有设计,所以就会出现各式各样的if判断语句,而这种类型的代码也是最容易引发bug的地方之一。究其根源,原因有两个:

2.1 耦合重度的模块之间存在网状依赖。

大量的网状依赖,导致开发人员想新增代码的时候基本上都需要走读一遍整个调用代码后,才敢新增代码,而且也很难新增代码,耗费时间长,容易有bug。举个例子:

图2.1,网状依赖示例图

为了更有代入感,我们假设每个类的定位跟将游戏后台里面的类代入一下,网状依赖中,会有部分代码出现环状链路依赖,我们通过一个更具体的例子来展现:

图2.2 环状链路依赖示例图

我们假设有这样的代码,ClassA::Func1接口调用 ClassB::Func1接口, ClassB::Func1接口调用 ClassC::Func1接口, ClassCFunc1 接口调用 ClassA::Func2接口,但是ClassA::Func2接口又调用了ClassA::Func1接口,这里形成了环状调用,为什么没有造成递归?是因为ClassA::Func1里面写了if语句去阻止递归。我们可以想象一下,如果开发人员需要给ClassA::Func1新增功能代码,其心智压力之大,可见一斑。

可能有人会说这种代码怎么可能会存在,但是实际情况是,这种代码还是会存在的。只不过在项目节奏放缓时或者出现问题时会被有心的同学重构掉。但是如果一个开发人员排期很紧,那么他的开发压力就很大了,因为重构需要测试保证,开发人员没有太多的时间去做这么多的开发,那么这就意味新的临时代码又将会再次增加,即Class::Func1的if语句将会再次大概率增加。从去除网状依赖的角度出发,我们需要解耦,需要从框架去考虑尽可能解耦各个模块。

1.2 老模块功能已经不完全符合现有需求,老模块负责人不愿意改动其代码,新功能代码必须僵硬适配老模块接口,引发项目内新增大量临时适配代码。

两个模块相互耦合,在业务团队内归属两个不同的开发人员负责。举个例子,开发人员A负责角色模块,开发人员B负责角色伙伴模块。由于项目需求的更迭,角色伙伴模块除了角色模块初始化时不满足现有功能外,其他都还是满足现有需求的。这个时候开发人员A希望开发人员B可以按照现有需求修改一下角色伙伴功能模块的代码,来解决初始化不符合规范的问题。但是开发人员B认为开发人员A可以通过写一些特殊适配接口来适配他的代码,两人有可能无法互相说服。

这最后的结果大概率就是开发人员A在角色伙伴模块写了很多if语句去解决初始化不规范的问题。这又再次导致了项目临时代码的增加。从这个角度出发我们也需要尽可能实现模块之间的解耦。

三、如何设计发布订阅(事件监听)设计模式

设计一个发布订阅(事件监听)设计模式,一定要充分站在开发者使用的角度去设计这个事件模式,开发人员用起来感觉简便清晰,那么便是设计的成功。

下面介绍一种设计思路,关键在于事件参数传参的设计(下面以C++语言作为具体语言进行举例):

图3.1 统一事件参数设计
// 触发事件模式,如LogicRole 触发事件:
class LogicRole {int HandleServiceA() {LogicEventDispatch::Instance().NotifyEvent(event_para);}
}// 事件分发调用
class LogicEventDispatch {int NotifyEvent() {for(auto handler : handlerList) {// step1. 派生事件先入队// step2. 处理主事件handler->OnEventUpdate(event_para);// step3. 处理派生事件队列}}
}// 事件处理, 开发人员只需要关注register操作与OnEventUpdate两个函数,然后便是自己的业务代码
class LogicRolePartner {int init() {LogicEventDispatch::Instance().RegisterEventHandler(event_a, this);LogicEventDispatch::Instance().RegisterEventHandler(event_b, this);}int OnEventUpdate(EventPara* event_para) {switch(event_para->type) {case event_a: {HandleEventA(event_para);break;}case event_b: {HandleEventB(event_para);break;}//...}}
}

四、发布订阅(事件监听)设计模式需要注意的问题

1、需要防止事件递归。事件再次触发同类型的业务需求是存在的。举个例子,为玩家添加一个道具会抛一个使用道具变化事件,同时这个道具是一个服务器自动使用的道具,那么当服务器底层将这个道具使用之后,便会再次抛出道具变化事件。或者任务完成事件会触发另外一个任务完成事件。所以事件触发抛同类型事件是有业务需求的,所以这里需要将派生事件全部先入队,处理完主事件,再处理派生事件,然后限制派生事件队列长度,当派生队列长度超过配置值时,需要立即告警(通过微信,企业微信等办公协作工具通知)以保证开发人员可以在测试环境发现问题并处理。

2、事件执行模块是无序的。使用这个模式不可以假定事件的执行顺序,举个例子某个事件会触发为玩家添加角色的功能,这个时候技能模块与角色模块同时关注这个事件,但是技能模块先接到这个事件,但是玩家角色模块还未初始化角色数据,这样会导致技能模块在处理事件的时候产生大量报错。通常来说这种情况出现比较少,可以在业务设计上来规避这个问题。

五、发布订阅(事件监听)设计模式不适用场景

1、业务设计流程要求顺序执行功能。有顺序执行的需求一般都是各种初始化逻辑,如角色初始化,因为像属性的初始化需要依赖于技能,所以这一块的逻辑代码需要按照顺序执行,直接调用各个模块的代码去执行初始化。如依次初始化角色技能,角色伙伴,角色属性等等。

2、事件处理不能包含异步流程。如果事件处理代码中包含异步流程如请求数据库,这个事件的处理需要依赖玩家Player对象的。当处理事件过程,请求数据库,当前事务或者协程切出。这个时候玩家Player对象被销毁,那么当数据库回包,协程恢复时就会找不到Player对象导致一系列报错与带来数据不一致的风险。

因为从目前项目看来,跨机(跨进程)事件的需求还是比较弱,所以框架层上的实现未考虑跨机事件的设计。但从需求上来看,如战斗单局需要与带有Player对象的gamesvr交互,或者gamesvr与一些活动服的交互,通过跨机事件来实现需求便是一个非常好的选择。所以这个是后续设计需要考虑的问题。

本文参考了一些内部文章不方便在此发出,在此仅以衷心的感谢致敬引用的文章的作者。

这里仅是一家之言,其实还有很多地方没说到,如这个设计模式的具体实现上,其实还有很多方式,包括跨机事件的设计上也是非常值得讨论的,后续再继续完善。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/444231.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode103. 二叉树的锯齿形层次遍历

给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。 例如: 给定二叉树 [3,9,20,null,null,15,7], 3 / \ 9 20 …

大型游戏后台实践浅谈

国家新闻出版署8月30日下发切实防止未成年人沉迷网络游戏的通知,要求从今天(9月1日)起,所有网络游戏企业仅可在周五、周六、周日和法定节假日每日20时至21时向未成年人提供1小时服务,其他时间均不得以任何形式向未成年人提供网络游戏服务。通知发布后,各大游戏厂商火速回…

如何使用弱网环境来验证游戏中的一些延迟问题

关于弱网 在当今移动互联网盛行的时代,网络的形态除了有线连接,还2G/3G/Edge/4G/Wifi等多种手机网络连接方式。不同的协议、不同的制式、不同的速率,使移动应用运行的场景更加丰富。 从测试角度来说,需要额外关注的场景就远不止断网、网络故障等情况了。对于弱网的数据定义…

使用nginx分片功能提升缓存效率,支持可拖拽式播放视频

Nginx的slice模块可以将一个请求分解成多个子请求,每个子请求返回响应内容的一个片段,让大文件的缓存更有效率。 HTTP Range请求 HTTP客户端下载文件时,如果发生了网络中断,必须重新向服务器发起HTTP请求,这时客户端已经有了文件的一部分,只需要请求剩余的内容,而不需要…

Nginx 配置TCP和UDP负载均衡

前言 Nginx除了以前常用的HTTP负载均衡外,Nginx增加基于TCP协议实现的负载均衡方法。 HTTP负载均衡,也就是我们通常所有“七层负载均衡”,工作在第七层“应用层”。而TCP负载均衡,就是我们通常所说的“四层负载均衡”,工作在“网络层”和“传输层”。例如,…

leetcode116. 填充每个节点的下一个右侧节点指针

116. 填充每个节点的下一个右侧节点指针 难度中等128 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: struct Node {int val;Node *left;Node *right;Node *next; } 填充它的每个 next 指针&am…

你的代码是否按照高内聚、低耦合的原则来设计的?

我们一直强调软件开发中要按照高内聚、低耦合的设计原则来做代码结构设计。c语言和c++不同,c语言面向过程、c++面向对象。 真正的项目中,要对业务升级,原来的业务函数需要保留,要保证老的功能继续维持,不能直接删除,这时候c语言面向过程,通常使用回调的方法。c+…

leetcode117. 填充每个节点的下一个右侧节点指针 II

给定一个二叉树 struct Node { int val; Node *left; Node *right; Node *next; } 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 初始状态下,所有 next 指针都被…

你担心大家会滥用的全局变量,大家(包括你自己)一定会滥用

前言 不要使用全局变量的道理大家都懂,基本上在大家学习编程过程中很早就会被教育到,但是有时候我们也会禁不住诱惑用到一些似非实是的全局变量,只不过这些全局变量会穿上马甲,让你不会一下看穿它的巨大危害,滥用全局变量会引申带来其它更为严重的结构性系统问题。…

Android Studio下载安装教程及开发环境搭建

Android Stuio是本次Google io的一大亮点啊,一大早起来就赶紧下载来玩玩了。。。 如果你不幸被墙了,可以去这个帖子下载,我已经上传到百度盘里面了。 [Android利器]Android Studio下载地址来啰 。。http://www.eoeandroid.com/thread-275380-…

leetcode124. 二叉树中的最大路径和

难度困难314 给定一个非空二叉树,返回其最大路径和。 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。 示例 1: 输入: [1,2,3]1/ \2 3输出: 6示例 2: 输入: …

深入剖析阻塞式socket的timeout

前言 网络编程中超时时间是一个重要但又容易被忽略的问题,对其的设置需要仔细斟酌。 本文讨论的是socket设置为阻塞模式,如果socket处于阻塞模式运行时,就需要考虑处理socket操作超时的问题。 所谓阻塞模式,是指其完成指定的操作之前阻塞当前的进程或线程,直到操作…

leetcode165. 比较版本号 超级重要的细节

比较两个版本号 version1 和 version2。 如果 version1 > version2 返回 1&#xff0c;如果 version1 < version2 返回 -1&#xff0c; 除此之外返回 0。 你可以假设版本字符串非空&#xff0c;并且只包含数字和 . 字符。 . 字符不代表小数点&#xff0c;而是用于分隔数…

游戏服务器缓存系统如何设计

前言 不管是在业界开源领域,还是内部分享中,很少会有专门针对游戏业务特征进行专门设计的组件、类库或者框架。我们从游戏的客户端方面来看,一款专业的游戏客户端引擎,已经是游戏开发的标配,flash,Cocos,Unity,Unreal等,但是服务器端,我们几乎找不到同样重量级的产品…

leetcode574. 当选者(SQL)

表: Candidate -------------- | id | Name | -------------- | 1 | A | | 2 | B | | 3 | C | | 4 | D | | 5 | E | -------------- 表: Vote ------------------- | id | CandidateId | ------------------- | 1 | 2…

使用KCP 加速游戏消息,让全球玩家流畅联网

定义 kcp协议是传输层的一个具有可靠性的传输层ARQ协议。 它的设计是为了解决在网络拥堵情况下tcp协议的网络速度慢的问题。 kcp力求在保证可靠性的情况下提高传输速度。 kcp协议的关注点主要在控制数据的可靠性和提高传输速度上面,因此kcp没有规定下层传输协议,一般用udp作为…

leetcode584. 寻找用户推荐人(SQL)

给定表 customer &#xff0c;里面保存了所有客户信息和他们的推荐人。 ----------------------- | id | name | referee_id| ----------------------- | 1 | Will | NULL | | 2 | Jane | NULL | | 3 | Alex | 2 | | 4 | Bill | NULL | …

剖析KCP以及KCP在游戏中是如何使用的

亲爱的各位读者你们好,由于前段时间忙于部分项目的重构和优化,未能及时更新文章,不少读者催更,哈哈,我还是很开心能抽出时间给大家再来分享下kcp的相关技术内幕,以及之前完善自己的网络库增加了KCP的客户端服务器收发支持(结尾会分享封装的客户端服务器C++源码)。 KCP概…

leetcode585. 2016年的投资(SQL)

写一个查询语句&#xff0c;将 2016 年 (TIV_2016) 所有成功投资的金额加起来&#xff0c;保留 2 位小数。 对于一个投保人&#xff0c;他在 2016 年成功投资的条件是&#xff1a; 他在 2015 年的投保额 (TIV_2015) 至少跟一个其他投保人在 2015 年的投保额相同。 他所在的城…

暴雪游戏走后,谁来接盘?对网易有何影响?

11月16日&#xff0c;暴雪娱乐公司宣布&#xff0c;由于与网易的现行许可协议将于2023年1月23日到期&#xff0c;将暂停在中国大陆的大部分暴雪游戏服务。这些暴雪游戏包括《魔兽世界》《炉石传说》《守望先锋》《星际争霸》《魔兽争霸 III&#xff1a;重制版》《暗黑破坏神 II…