用組件來重構你的遊戲實體

英語原文: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/

譯文原文: http://blog.csdn.net/zhanghefu/article/details/6680620



進化遊戲的層次結構

  - 用組件來重構你的遊戲實體

直到最近幾年,遊戲程序員一直使用深層次結構的類表示遊戲實體。現在的潮流開始逐漸從深層次的結構,到僅僅是把遊戲實體對象作爲聚合組件的方向轉變。這篇文章解釋了這些轉變意味着什麼,並且探討了用這種方式帶來的的好處和實際情況中的使用。我將會描述我個人的一些經驗,怎樣在大項目中實現這個系統,當然也包括怎樣去賣你的方案給別的程序員和經理。

 

遊戲實體

不同的遊戲有不同的需求,就像遊戲實體應該需要什麼一樣。但是在大多數的遊戲裏,實體的概念是十分的相似的。一個遊戲實體就是一個在遊戲世界裏的對象,通常這個對象對於玩家來說是可見的,並且通常它還能四處移動。

一些實體的例子:

l        子彈

l        小轎車

l        坦克

l        手榴彈

l        

l        英雄

l        行人

l        外星人

l        噴氣式飛行器

l        醫療包

l        石頭

實體通常可以做很多事情。下面是一些事情你也許想要實體去做的:

l        運行一個腳本

l        移動

l        表現的像個死板的東西

l        發射粒子

l        播放特定的聲音

l        能被玩家放在揹包裏

l        能被玩家穿上

l        爆炸

l        表現的有磁性的

l        被玩家瞄準

l        沿着一條路徑走

l        動畫

傳統的深層次結構

傳統的表示一組實體集的方式就像是在分解我們想要去表的實體集。這樣做通常開始的意圖是好的,但是隨着遊戲的開發進度這些東西經常要變動尤其是當一個遊戲引擎被不同的遊戲重新使用時。我們通常最後的設計出如 B-1那樣,但是實際上的類層次結構比圖中節點還要多。

 

B-1

隨着開發的進行,我們通常需要增加很多不同的功能到實體上。對象必須要麼封裝自己封裝功能,要麼從有那個功能的別的對象那裏繼承過來。經常性的功能被加載接近類層次結構的根節點上,比如說CEntity類。這樣做有一個好處,就是所有派生類都能有那些功能。但是不好的地方是會被這些類帶來相關的開銷。

即使是非常簡單的對象比如石頭或者是手榴彈,到最後會有大量的額外功能(和相關的成員變量,或者是不必要執行的成員函數)。傳統的遊戲對象層次結構經常到最後要創建一個被稱作團跡”(胖球)(the blob)的東西。胖球是經典的反模式之一,表現爲一個巨大的單類(或者是有大量的分支在類的層次結構上),擁有大量的複雜的互相交織的功能。

當胖球反模式經常在對象層次結構的根節點附近出現,它也就顯現在葉子節點上了(譯註:因爲葉子節點是繼承自根的)。最有可能的候選者因該是表示玩家的類。由於遊戲通常是針對單一角色而編寫的程序,因此表示角色的對象經常有大量的功能。這經常是實現爲在一個類裏比如CPlayer類,有大量的成員函數。

實現這麼多功能在層次結構的根節點附近的結果就是給葉對象大量不需要功能的過重包袱。不管怎麼樣,用相反的實現方法,在葉子節點上實現大量的功能,同樣是不幸的結果。功能現在被分解了,所以只有專門爲那個對象編程的特定功能才能使用它。程序員經常複製一樣的代碼到已經被不同的對象實現的鏡子函數裏。最終,需要重新組織類的層次結構這種骯髒的重構來移動和組合功能。

先來一個例子吧,有一個對象在在物理作用下表現爲剛體的功能。不是所有的對象需要做到這樣。你可以在圖B-1裏看到的那樣,我們僅僅讓CrockCGrenade類從CRigid類派生。如果我們想要將此功能應用到車子上會發生什麼呢?你不的不把CRigid類移到層次結構的上面去,讓它變得更像我們以前看到的根部的重型胖子模式,所有的的功能都被串成一條類的窄鏈子從其他最先開始繼承的類開始起。

 

聚合組件

組件方式,現在越來越得到現在的遊戲開發的認可,是一種把不同的功能分開放到不同的獨立於其他組件的組件上的方法。傳統的對象層次結構被免除了,並且一個對象現在被創建爲爲一個獨立的組件的聚合(積聚物)。

每個對象現在只有它需要的功能了。任何不同的新共嫩被實現爲增加一個組件。

一個由聚合組件組成的對象系統能有3種方式實現,可以被看成將胖球對象層次結構轉移到一個組合對象上去的不同階段。下面將介紹一下這3個階段。

 

對象作爲組織胖球

一種通常重構胖球對象的方法是將它的功能分散到不同的子對象上去,然後被第一個對象所引用。最終,父系的胖球對象被一系列的指向其他對象的指針代替,最終胖球對象的成員函數編程了這些子對象上函數的接口函數。

這也許事實上是一個合理的解決方案,如果你的遊戲對象裏的功能在一個合適小的範圍內,或者如果時間是有限的。你可以簡單實現任意的對象聚集,通過允許一些子對象爲空(通給一個NULL指針給它們)。假設沒有太多的子對象,那麼這仍然允許你有一個輕型的沒有實現一個管理此對象的組合框架的僞組合對象的優勢。

不足之處是,這仍然在本質上是一個胖球。所有的功能人然被封裝在一個大對象裏。這不像是你完全的分解胖球對象到純的子對象那樣,所以你仍然遺留了一些重要的開銷,仍然會讓你輕型的對象變重。你仍然有不斷檢查所有空指針,以便看看是否需要更新的開銷。

 

對象作爲組件容器

下一個階段是分解每個組件(上一節例子裏的“子對象”)成共享一個公共的基類的對象,因此我們可以存儲一個對象的列表在對象裏。

這是一個過度的解決方法,我們仍然有表示遊戲實體的根“對象”。不管怎樣,它應該是一個合理的解決方案,或者確實是在實踐中是可行的方案,如果一大部分的代碼庫中需要這種概念的遊戲對象作爲具體對象的話。

你的遊戲對象然後變成了一個接口對象,充當了在你遊戲裏的遺留代碼之間橋的作用,並且還是新系統的組合對象。如果時間允許,你最終將會把遊戲實體對象作爲整體式對象的概念消除掉。相反,訪問對象越來越直接的通過它所在的組件了。最終,你能夠將其轉換到純聚合了。

 

對象作爲純聚合

在最終的佈置圖裏,一個對象簡單是各個部分的和。圖B-2顯示了一個方案,每個對象都是由許多不同的組件組成的。這裏沒有所謂的“遊戲實體對象”。每一列在圖標中都表示同一組件,每一行因此都能表示一個對象。組件自己也可以看成是和組成它們的對象是獨立的。

 

B-2

實踐經驗

我第一個用組件實現的對象的組合系統是我在Neversoft公司做Tony Hawk系列遊戲的時候做的。我們的遊戲對象系統一直伴隨着三個連續發佈的遊戲而發展,指導我們有了一個遊戲對象的層次結構來重組我先前提到的胖球反模式。它遭受着所有同樣的問題:對象傾向於重量級的。對象有不必要的數據和功能。有時不必要的功能讓遊戲變慢。功能有時在不同樹的分支上重複。

我在sweng-gamedev的郵件列表裏聽說過這個關於“基於對象的組件”系統的新式發明。我覺得那聽起來是一個好主意。我於是開始重新組織代碼,兩年以後把它完成了。

爲什這麼長的時間?因爲,首先我們在以每年一個的速度艱苦的做出Tony Hawk遊戲,所以只有很少的時間讓我們投入到重構上。第二,我錯誤的計算了問題的規模。一個三年時間長的代碼羣已經包含大量的代碼。大量的代碼一年一年的逐漸變成了某種不靈活的代碼。由於代碼依賴於遊戲對象成爲遊戲對象,尤其是的某些遊戲對象。那說明了有大量的工作要去做,才能使得所有的東西都已組件方式工作。

 

預期的阻力

我第一遇到的問題就是怎樣試着解釋這個系統給其他的程序員。如果你不是特別的熟悉對象組合和聚合的事情,那麼會被認爲是無意的,不必要的複雜,不必要的多餘工作,讓你受備受打擊。程序員已經在傳統系統的對象層次結構上工作了很多年,已經非常習慣那種工作方式了。它們甚至變得擅長於那種方式來,能解決那些出現的問題了。

把這個方案賣給經理也是一個困難。你需要能夠用平實的語言準確的描述,這個方案怎樣能夠讓遊戲完成的更快。下面是一段的因該說的話:

“當我們加入新的特性到遊戲裏時,那會花費很長的時間去完成,將會導致很BUGS。如果我們採用這種新的組件對象的東西,它能讓我們加入新的特性更快,會有更少的BUGS

而我採用是一種悄悄的方式。我首先和一些程序員單獨討論這個主意,最後說服他們這是一個好主意。我然後實現了通用組件的基本框架,並且還實現了遊戲對象功能的很小的一部分作爲組件。

我然後把這些成果呈現給剩下的程序員。他們有一些疑惑和牴觸,但是由於它已經實現了並且它在那裏工作不是一個大的爭議。

 

緩慢的進展

當框架被鏈接上了,從靜態的層次結構到對象組合的方便性顯現的很緩慢。那是一個吃力不討好的工作,即使你花了很多小時,很多天將代碼重構成一些看起來像樣的東西,但其和被替換的代碼沒有什麼兩樣。而且,我們還在做這個事情的時候,我們仍然在爲下一個遊戲實現新的功能。

在早些時候,我們撞上了重構我們最大的類滑雪者類的問題。由於它包含有大量的功能,它甚至在一段時間幾乎無法重構一小點。事實上,它也無法被重構除非其他在遊戲裏的對象系統已經服從了組件方式了。話又說回來,其他那些對象系統也不容易被組件化,除非滑雪者已經是一個組件了。

這裏的解決方案是創建一個“胖球組件”。這是一個單獨的巨型組件,封裝了大量滑雪者類的功能。少量的其他胖球組件也需要被用在別的地方。我們最終硬是將對象系統塞進了組件裏。當這個事情到位了,胖球組件能被逐級的重構成更多的原子組件。

 

結果

一開始重構的結果不是那麼明顯。但是隨着時間的推移,代碼變得越來越清晰並且變得更容易維護,功能都被封裝到分散的組件裏了。程序員開始用更少的時間創建新類型的對象,僅僅簡單的組合一些組件然後再加一個新的。

我們創建了一個數據驅動的對象創建系統,因此整個新類型的對象都能被設計人員創造。這被證明對於快速創建和配置新類型的對象是非常有價值的。

最終程序員開始(以不同的速度)接受組件化系統了。並且他們變得非常熟練的擅長通過組件來增加新的功能了。通用的接口和嚴格的封裝使得BUGS減少了,代碼也更容易閱讀,更容易維護和重用。

 

實現細節

給每一個組件一個通用的接口意味着繼承自同一個帶虛函數的基類。這會帶來額外的開銷。但不要因爲這一點而使你反對這種方法,節約的開銷和對象的簡單性相比是不不重要的。

由於每個組件都有一個公共的接口,非常容易的就可以增加額外的調試成員函數給每個組件。這使得增加一個能導出組件的組合對象的可讀信息的診斷器對象更容易了。然後,這可以被進化成一個複雜功能的遠程調試工具,總能夠得到幾乎所有類型的遊戲對象的最新信息。這也許在傳統的層次結構的系統裏去實現和維護是十分的令人厭惡的。

理想情況下,組件應該互相不知道到對方。不管怎麼樣,在現實世界裏,總是有特定組件間的依賴關係。性能問題,也決定了組件應該能夠快速的訪問其他組件。開始的時候,我們讓所有組件的引用都是通過組件管理器的,但是當開始時只用了5%CPU時間,我們允許組件存貯指向其他對象的指針,並且直接調用在其他組件裏的成員函數。

在組件裏,怎樣組合對象的順序是非常重要的。在我們一開始的系統裏,我們把組件作爲鏈表存儲在一個容器對象裏。每個組件有一個更新函數,每個對象每次迭代組件列表時被調用。

由於對象創建是數據驅動的,那樣會造成麻煩的,如果在鏈表裏的組件不是期望的順序的話。如果一個對象更新物理相關內容在動畫相關內容之前,但是另外一個對象更新動畫相關內容在物理相關內容之前,這樣他們就會互相失去同步。互相依賴關係像這樣的必須找出來,然後在代碼裏定義強制規則。

 

結尾

用組件把從胖球風格的對象層次結構轉變成組合對象結構是我所做的最好的決定之一。開始的結果是讓人失望的,它花費了太多時間去重構現有的代碼。不管怎麼樣,最後的結果是非常值得,輕型的,靈活的,健壯,和可重用的代碼。


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