十全乾貨:核心遊戲系統架構設計

十全乾貨:核心遊戲系統架構設計

 首先先來定義一下什麼是我這裏說的核心遊戲系統,一般來說,遊戲可以大致分爲兩個部分,一個部分是我這裏指的核心遊戲部分,比如FPS裏的射擊戰鬥部分,或者如LOL裏的戰鬥對抗部分,又或者是體育類遊戲裏的比賽部分等等。

這些是遊戲裏的主要玩的點,核心遊戲部分可以很重,佔到玩家80%以上的遊戲時間,也可以很輕,甚至沒有,像現在很火的列王的紛爭(COK),幾乎就是沒有什麼核心遊戲部分。另一部分就是外圍的輔助系統,比如裝備,任務,社交等等,這部分也有玩點和設計用意,這兩個部分相輔相成組成了大部分遊戲的主體框架。而今天要聊的就是第一部分的核心遊戲系統。

  從開始做遊戲到現在,我大部分的工作是專注在引擎以及核心遊戲系統部分,所以今天就想來聊聊如何來設計核心遊戲系統。當然這個設計不一定適用於所有的遊戲,僅僅是我個人的經驗之談,希望能給大家一些參考的價值。

  大多數情況下,核心遊戲系統都比較複雜,牽涉到很多系統之間的協作,也和策劃的需求有相當緊密的聯繫,國外公司一般稱這類程序員爲Gameplay programmer,國內公司這種職位相對較少,一般就以泛指的客戶端程序員代替了,但和做外圍系統的程序員不同,真正的Gameplay programmer需要對於AI系統,動畫系統,物理系統都有一定的瞭解,因爲這是核心遊戲部分都會涉及的領域。正因爲核心遊戲系統的複雜性,所以必須要有一個適合的,靈活的架構來支持,拋開一些基本的優點,諸如可擴展性,低耦合等等要求不說,我最直觀的感受,就是以下兩點好處:

● 多人合作:一個好的架構可以將系統進行合理的拆分,這樣的話就便於多人協作,對於核心遊戲系統來說,一般是不可能一個人單槍匹馬的去完成的,所以如何去拆分任務讓更多的人蔘與,對於項目而言是相當有利的。對於現在的AAA的遊戲來說,單單一個主角,可能就會有將近10個程序員在一起製作,包括AI,行爲,動畫等等,所以好的架構可以保證工作效率隨着人數的增加而得到提升;
● 留有“揮霍”的空間:架構是很難完美的,因爲在開始設計的時候,所有的需求並不明確,特別是核心遊戲系統,可能會推倒重來,重構很多次。而當遊戲方向定下來了之後,一些策劃的改動或者擴展,也會使得以前的架構在某些情況下變得不是很適用,這個時候就會需要一些對於特殊情況的處理,也就是所謂的“hack”,好的架構會讓我們在開發後期,在不重構的情況下,有餘地進行適當的“hack”,而不是在一開始就“hack”到底,導致bug滿天飛。

好,接下來開始說說設計思路。

  解構一個複雜的系統,有一個很好(不是唯一)的辦法,就是“分層結構”(Layered structure),也就是把一個複雜系統,分成一層一層的結構,每一層都做每一層自己的事情,並且每一層都是單向依賴。這樣可以把一個網狀的,如同亂線團一樣的複雜系統,梳理的非常的清楚。這樣的例子其實很多,比如學計算機的人都很熟悉的OSI網絡七層架構,這就是一個非常好的,把複雜問題層次化的典型例子,它使得每一層都可以獨立設計,而且可以有明確的設計目標,層與層之間的接口也變得非常清晰。

還有一個遊戲的例子,就是遊戲的架構,遊戲其實也是遵照這層次化的設計思路來設計的,雖然不像OSI那樣有一個標準化的結構,但是大部分遊戲可以分爲核心層(Core),引擎層(Engine),遊戲類型層(Game Genre),遊戲層(Game),像現在一般的商用的遊戲引擎,基本就做到核心層和引擎層,再往上就是使用引擎的人自己設計和實現了,像一些大公司可能會有一些積累,就會根據不同的遊戲類型在引擎層的之上抽象出遊戲類型層,比如體育類遊戲,射擊類遊戲等等,然後再開始開發實際的遊戲產品。這種分層的架構設計就可以幫助我們把複雜的系統進行解構,從而實現每個子系統或者模塊的功能單一化。

核心遊戲系統架構也可以用這樣的思路來設計,這樣每一層都可以由不同的人來負責,如果實現的話,在一層當中也可以進行任務分工,那下面我就根據執行順序,自上而下一層一層來描述。

  總共的架構分爲五層

第一層:更新/收集世界信息

  這部分主要是要設計兩個部分,一個是知識池(Knowledge Pool),另一個就是感知器(Sensor)。聽上去很高大上,其實概念上很簡單,知識池可以理解爲就是世界信息的數據存儲,比如某一個智能體需要一個這樣的數據,“誰是離我最近的人”,這個數據就可以存下來,方便獲取

這種數據存儲的數據結構,可以根據不同的情況去選擇,用key-value的黑板格式,或者自定義的數據類型都可以,也可以分成多個知識池來管理不同類型的數據,設計的關鍵就是要有清晰的世界信息獲取方式。

  感知器的話,就可以理解爲具體的獲取數據的方法,可以定義一個感知器的接口類
1. interface ISensor {
2. Update()
3. }

這樣就可以把這個感知器註冊到一個感知器的管理器中,當收集所有世界信息的時候,只要遍歷一遍這些感知器,就可以完成對於世界信息的收集工作:

  1. foreach(s in sensers){
  2. s.Update()
  3. }

感知器也可以分爲兩種,一種是全局的感知器,這種可以看成是對於遊戲整個世界,或者關卡的抽象,比如勢力圖

還有一種是個體感知器,比如聽力,視野等等

當然,這只是一種思路,也可以不定義接口,而是寫成不同的數據更新方法。這就是第一層,主要的功能就是爲下層預備數據。

  第二層往下,都是針對單個智能體的更新,也就是說需要對每一個智能體執行更新操作。關於智能體的行爲,很多時候容易寫的一團糟,又要決策,又要運動,又有動畫,還要處理物理,有些系統呢,需要每幀更新,比如位置,有些呢,又不需要更新的這麼勤快,比如決策,所以在設計上我把它分成幾個層次,決策層,請求層,行爲層,運動層。

第二層:更新決策層(做什麼)- What to do

決策層就是負責來決策此時該智能體應該要做什麼,比如我要走到某個位置,我要攻擊,放技能等等,可以說,這就是傳統所說的人工智能AI部分,這部分只根據當前所有的世界信息,產生“做什麼”的決策,決策的內容會封裝在一個“請求”(Request)的結構中,繼續向下傳遞:

  這部分的結構可以有多種選擇,狀態機,行爲樹,甚至神經網絡都可以,但是有兩個要點

決策的時機:也就是什麼時候進行決策,這裏就可以用來控制決策的頻率,比如離玩家很遠的人可以降低決策頻度,離玩家近的人,可以提高決策頻度等等,類似與這種的控制都應該在這一層中得到支持和實現。
決策請求的類型:這一層的輸出可以看成是所有該智能體可以做的決策的總和,所以千萬不要把一些下層的行爲放在這裏,比如尋路,接下來會說到,尋路並不是決策層的決策行爲,它只是來處理“移動”這個決策的一個方法。還有比如選擇動畫,也不應該是決策層所要關心的內容。

  還有一種特殊的模塊是屬於這一層,那就是玩家輸入,玩家的輸入說起來,其實也是一種決策,只是這個決策是通過玩家來做出的。行爲樹就很容易處理這個情況,將玩家輸入和AI決策可以融合成一體讓所有的智能體共用:

第三層:更新請求層

上面說到,決策層產生的輸出是“請求”,請求是一種自定義的數據結構,包含所以該決策所需要傳遞的決策信息。那爲什麼要更新請求層呢?直接把當前請求傳遞給下一層不就好了嗎?這一層的功能抽象,也是我在實踐中的經驗,總結一句話,“請求層”就類似於一個“防火牆”,由它來“過濾”,那些請求會被繼續往下傳遞到行爲層。

  我們可以先來看一下請求層的設計,請求層的設計,借鑑了渲染中的“雙緩衝”結構,把請求分爲,前端請求(Foreground Request),後端請求(Background Request),前端請求就是當前智能體正在執行的請求,因爲一個決策請求可能需要多幀才能完成(想象一下,移動到某一個點這個請求,就需要一段時間才能完成),後端請求就是準備執行決策請求,當一定條件滿足後,就可以做了一個“Flip”的操作(前後互換),把後端請求變成前端請求,這樣的話,後一層就會執行這個請求,從而改變行爲了。

細心的同學會發現,在上面的描述中,有一個地方值得回味,就是Flip操作是,“當一定條件滿足後”,而這個條件的監測,就是這裏更新請求層的時候需要做的事情了,其實每一個決策是存在潛在的優先級的,這個優先級和策劃的設計有關,比如我當前正在執行一個攻擊的請求,這個時候,新的請求是一個釋放技能,此時策劃要求,技能釋放能夠打斷當前的攻擊行爲,那這個判斷邏輯就可以寫在這一層中,使得當這個條件滿足時,可以立即切換請求。這裏的設計一般採用配優先級表,和基於規則的(Rule-based)的實現方式。

  有了這一層的邏輯抽象,就可以保證它的上層和下層都不需要關心決策能否被執行,而只要關心自身的決策/行爲邏輯就可以了,大大降低了上下層的實現複雜度。由於這部分邏輯相對比較繁瑣,所以把這些繁瑣的邏輯集中在一起,也是一種理想的設計思路。

 第四層:更新行爲層(怎麼做)- How to do

 就像補充裏所描述的,行爲層的職責就是怎麼做,也就是如何去完成上層經過決策,經過規則的邏輯判定,最終“勝出”的那個前端請求。像前面提到的尋路,或者選擇需要播放的動畫,都是在這一層所完成的工作,這層的實現同樣可以用行爲樹,或者狀態機,不過我還是推薦用行爲樹,因爲行爲樹可以擴展和處理更復雜的行爲邏輯,比如隨機,序列,並行等等。某些請求可能不是用單個行爲可以完成的,需要多個行爲的配合,比如完成一個技能釋放,需要先集氣,然後再釋放,類似這樣的行爲,就可以用行爲樹來實現了,更棒的是,集氣這個行爲還能被共用。

在這一層中,會產生一系列的輸出,有特效,有動畫,可能還有聲音等等,有一個很重要,也是必不可少的,那就是運動信息(Kinematic Information),這也是智能體最終呈現的樣子,這部分內容包括空間信息(位移,旋轉,縮放)和動畫信息,某些情況下,智能體的空間信息可以通過物理計算在這一層直接更新,動畫信息直接調用引擎的播放接口即可,但有時候這種處理還不夠,那就需要第五層,運動層的參與。

第五層:更新運動層

遊戲中物體的移動有兩種方式,一種是動畫跟隨物理,比如爲了解決移動中的滑步問題,我們可以做多個移動的動畫,然後根據速度做融合,這樣可以調出一個滑步不明顯的表現,還有一種就是root motion,也就是物理跟隨動畫,就是物體的移動和旋轉完全跟隨動畫中的效果,這樣可以解決一些物理沒有辦法模擬的複雜運動。

  有些時候,我們需要混合使用這兩種方式,並且在位移過程中需要加上一些修正(比如爲了解決同步問題),這個時候,就需要用運動層來實現。這一層的輸入,就是行爲層產生的運動信息,輸出自然就是智能體最終的空間信息和動畫了。

  在實現上,建議對這兩種方式進行封裝,這樣可以對於上層來說,接口就相對統一了。

  有了這五層的設計,整個核心遊戲系統的更新循環就完成了,並且每一層的功能職責和輸入輸出都有了明確的定義,如下圖:

每一層具體的設計可以仁者見仁,智者見智,並且也和具體的遊戲有關,但是整體的架構基本就可以參考這樣的思路,至少以我的實踐來看,不會導致結構混亂,也可以更好的進行分工和合作。

  最後再聊一個關於核心遊戲部分網絡同步和回放系統的問題(同步和回放是差不多的東西,回放只是把同步的東西存下來而已)。

  其實如果有了上面的架構設計,理解網絡同步就很簡單了。

  如果同步放在第二層,那就是採用的“同步輸入”(Input synchronization)的方式

如果在第三層,那就是“同步命令”(Command synchronization)的方式

如果放在第四層/第五層,那就是“同步狀態”(State synchronization)的方式

這三種方式是越往下傳輸的數據越多,但是“失同步”(Out of synchronization)的風險就越小。當然不同的方式在具體實現上,還是有很多值得討論的地方,這裏就不多說了。如果客戶端和遊戲服務器採用相同的語言,那就可以很方便的在單機遊戲和網絡遊戲間切換,在單機模式下,只是本地和本地通信罷了,FPS遊戲很多都是這樣去實現的,其實在單機模式下,內部也是一個CS的架構,而如果需要一個服務器的版本,只是加一個宏去編譯而已。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章