原文鏈接:http://codemacro.com/2012/04/23/game-server-info-1/
我們的邏輯服務(wù)器(Game Server,以下簡稱GS)主要邏輯大概是從去年夏天開始寫的。因為很多基礎(chǔ)模塊,包括整體結(jié)構(gòu)沿用了上個項目的代碼,所以算不上從頭開始做。轉(zhuǎn)眼又快一年,我覺得回頭總結(jié)下對于經(jīng)驗的積累太有必要。
整體架構(gòu)
GS的架構(gòu)很大程度取決于游戲的功能需求,當然更受限于上個項目的基礎(chǔ)架構(gòu)。基礎(chǔ)架構(gòu)包括場景、對象的關(guān)系管理,消息廣播等。
需求
這一回,程序員其實已經(jīng)不需要太過關(guān)心需求。因為我們決定大量使用腳本。到目前為止整個項目主要還是集中在技能開發(fā)上。而這個使用腳本的度,就是技能全部由策劃使用腳本制作,程序員不會去編寫某個具體技能,也不會提供某種配置方式去讓策劃通過配置來開發(fā)技能。這真是一個好消息,不管對于程序員而言,還是對于策劃而言。但后來,我覺得對于這一點還是帶來了很多問題。
實現(xiàn)
基于以上需求,程序員所做的就是開發(fā)框架,制定功能實現(xiàn)方案。腳本為了與整個游戲框架交互,我們制定了“觸發(fā)器“這個概念,大概就是一種事件系統(tǒng)。
這個觸發(fā)器系統(tǒng),簡單來說,就是提供一種“關(guān)心“、”通知“的交互方式,也就是一般意義上的事件機制。例如,腳本中告訴程序它關(guān)心某個對象的移動,那么當程序中該對象產(chǎn)生移動時,就通知腳本。腳本中可以關(guān)心很多東西,包括對象屬性,其關(guān)心的方式包括屬性值改變、變大、變小,各種變化形式;對象開始移動,移動停止;對象碰撞,這個會單獨談?wù)劊欢〞r器等。
除了觸發(fā)器系統(tǒng)外,還有個較大的系統(tǒng)是游戲?qū)ο蟮膶傩韵到y(tǒng)。對象的屬性必然是游戲邏輯中策劃最關(guān)心最容易改動的模塊。既然我們程序的大方向是盡可能地不關(guān)心策劃需求,所以對象屬性在設(shè)計上就不可能去編寫某個具體屬性,更不會編寫這個屬性相關(guān)的邏輯功能。簡單來說,程序為每個對象維護一個key-value表,也就是屬性名、屬性值表。該表的內(nèi)容由腳本填入,腳本享有存取權(quán)限。然后腳本中就可以圍繞某個屬性來編寫功能,而程序僅起存儲作用。
第三,怪物AI模塊。AI模塊的設(shè)計在開發(fā)周期上靠后。同樣,程序不會去編寫某類AI的實現(xiàn)。程序提供了另一種簡單的事件系統(tǒng),這個系統(tǒng)其實就是一個調(diào)用腳本的方案。當關(guān)于某個怪物發(fā)生了某個事件時,程序調(diào)用腳本,傳入事件類型和事件參數(shù)。這個事件分為兩類:程序類和腳本類。腳本類程序不需關(guān)心,僅提供事件觸發(fā)API。程序類事件非常有限:怪物創(chuàng)建、出生、刪除。
除了以上三塊之外,還有很多零散的腳本交互。例如游戲?qū)ο髮傩猿跏蓟巧M入游戲,角色進入場景等。這些都無關(guān)痛癢。
接下來談一些關(guān)鍵模塊的實現(xiàn)。
定時器
整個GS的很多邏輯模塊都基于這個定時器來實現(xiàn)。這個定時器接收邏輯模塊的注冊,在主循環(huán)中傳入系統(tǒng)時間,定時器模塊檢查哪些定時器實例超時,然后觸發(fā)調(diào)用之。這個主循環(huán)以每幀5ms的速率運行,也即幀率1000/5。
這個定時器是基于操作系統(tǒng)的時間。隨著幀率的不同,它在觸發(fā)邏輯功能時,就必然不精確。游戲客戶端(包括單機游戲)在幀率這塊的實現(xiàn)上,一般邏輯功能的計算都會考慮到一個dt(也就是一幀的時間差),例如移動更新,一般都是x = last_x + speed * dt。但,我們這里并沒有這樣做。我們的幾乎所有邏輯功能,都沒有考慮這個時間差。
例如,我們的移動模塊注冊了一個固定時間值的定時器,假設(shè)是200ms。理想情況下,定時器模塊每200ms回調(diào)移動模塊更新坐標。但現(xiàn)實情況肯定是大于200ms的更新頻率,悲劇的是,移動模塊每次更新坐標都更新一個固定偏移。這顯然是不夠精確的。
更悲劇的是,定時器的實現(xiàn)中,還可能出現(xiàn)跳過一些更新幀。例如,理論情況下,定時器會在系統(tǒng)時間點t1/t2/t3/t4分別回調(diào)某個邏輯模塊。某一幀里,定時器大概在t1回調(diào)了某邏輯模塊,而當該幀耗時嚴重時,下一幀定時器模塊在計算時,其時間值為t,而t大于t4,此時定時器模塊跳過t2/t3。相當于該邏輯模塊少了2次更新。這對于移動模塊而言,相當于某個對象本來在1秒的時間里該走5格,但實際情況卻走了1格。
當然,當游戲幀率無法保證時,邏輯模塊運行不理想也是情有可原的。但,不理想并不包含BUG。而我覺得,這里面是可能出現(xiàn)BUG的。如何改善這塊,目前為止我也沒什么方案。
移動
有很多更上層的模塊依賴移動。我們的移動采用了一種分別模擬的實現(xiàn)。客戶端將復(fù)雜的移動路徑拆分為一條一條的線段,然后每個線段請求服務(wù)器移動。然后服務(wù)器上使用定時器來模擬在該線段上的移動。因為服務(wù)器上的阻擋是二維格子,這樣服務(wù)器的模擬也很簡單。當然,這個模塊在具體實現(xiàn)上復(fù)雜很多,這里不細談。
碰撞檢測
我們的技能要求有碰撞檢測,這主要包括對象與對象之間的碰撞。在最早的實現(xiàn)中,當腳本關(guān)心某個對象的碰撞情況時,程序就為該對象注冊定時器,然后周期觸發(fā)檢測與周圍對象的距離關(guān)系,這個周期低于100ms。這個實現(xiàn)很簡單,維護起來也就很簡單。但它是有問題的。因為它基于了一個不精確的定時器,和一個不精確的移動模塊。
首先,這個檢測是基于對象的當前坐標。前面分析過在幀率掉到移動更新幀都掉幀的情況下,服務(wù)器的對象坐標和理論情況差距會很大,而客戶端基本上是接近正確情況的,這個時候做的距離檢測,就不可能正確。另一方面,就算移動精確了,這個碰撞檢測還是會帶來BUG。例如現(xiàn)在檢測到了碰撞,觸發(fā)了腳本,腳本中注冊了關(guān)心離開的事件。但不幸的是,在這個定時器開始檢測前,這兩個對象已經(jīng)經(jīng)歷了碰撞、離開、再碰撞的過程,而定時器開始檢測的時候,因為它基于了當前的對象坐標,它依然看到的是兩個對象處于碰撞狀態(tài)。
最開始,我們直覺這樣的實現(xiàn)是費時的,是不精確的。然后有了第二種實現(xiàn)。這個實現(xiàn)基于了移動的實現(xiàn)。因為對象的移動是基于直線的(服務(wù)器上)。我們就在對象開始移動時,根據(jù)移動方向、速度預(yù)測兩個對象會在未來的某個時間點發(fā)生碰撞。當然,對于頻繁的小距離移動而言,這個預(yù)測從直覺上來說也是費時的。然后實現(xiàn)代碼寫了出來,一看,挺復(fù)雜,維護難度不小。如果效果好這個維護成本也就算了,但是,它依然是不精確的。因為,它也依賴了這個定時器。
例如,在某個對象開始移動時,我們預(yù)測到在200ms會與對象B發(fā)生碰撞。然后注冊了一個200ms的定時器。但定時器不會精確地在未來200ms觸發(fā),隨著幀率的下降,400ms觸發(fā)都有可能。即便不考慮幀率下降的情況,它還是有問題。前面說過,我們游戲幀保證每幀至少5ms,本來這是一個限幀手段,目的當然是避免busy-loop。這導(dǎo)致定時器最多出現(xiàn)5ms的延遲。如果策劃使用這個碰撞檢測去做飛行道具的實現(xiàn),例如一個快速飛出去的火球,當這個飛行速度很快的時候,這5ms相對于這個預(yù)測碰撞時間就不再是個小數(shù)目。真悲劇。
技能
雖然具體的技能不是程序?qū)懙模绨褞缀跛芯唧w邏輯交給策劃寫帶來的悲劇一樣:這事不是你干的,但你得負責它的性能。所以有必要談?wù)劶寄艿膶崿F(xiàn)。
技能的實現(xiàn)里,只有一個技能使用入口,程序只需要在客戶端發(fā)出使用技能的消息時,調(diào)用這個入口腳本函數(shù)。然后腳本中會通過注冊一些觸發(fā)器來驅(qū)動整個技能運作。程序員一直希望策劃能把技能用一個統(tǒng)一的、具體的框架統(tǒng)一起來,所謂的變動都是基于這個框架來變的。但策劃也一直堅持,他們心目中的技能是無法統(tǒng)一的。
我們的技能確實很復(fù)雜。一個技能的整個過程中,服務(wù)器可能會和客戶端發(fā)生多次消息交互。在最初的實現(xiàn)中,服務(wù)器甚至?xí)刂瓶蛻舳说募寄芴匦А⑨尫艅幼鞯雀鞣N細節(jié);甚至于服務(wù)器會在這個過程中依賴客戶端的若干次輸入。
下一篇我將談?wù)勔恍┯龅降膯栴}。