一個通用游戲后臺的設計模式實踐總結

搞業務開發的時候,發現有一些代碼的開發會讓人感覺非常簡便舒服,有一些代碼的開發卻有時候會讓人感覺心智負擔比較大。

逐步總結的過程中,發現讓開發人員寫起來感覺舒服的代碼,大概率是因為當前模塊與其他模塊代碼耦合度低,開發人員無需花費過多的精力去關注其他模塊的實現,只需要專注于當前自己的功能實現即可。

而通過實地對多個項目代碼設計的對比,發現都有一個非常通用的設計模式,后面再通過閱讀一些經驗分享文章中,發現大部分后臺項目也都運用這個模式,充分實現了模塊之間的解耦,極大提高了開發人員的幸福感,這個通用的設計模式便是:發布訂閱模式(游戲后臺通常也叫事件監聽模式 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.pswp.cn/news/444231.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/444231.shtml
英文地址,請注明出處:http://en.pswp.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…