用依賴注射模式實現快速安全的遊戲對象原型

英語原文: http://www.ic.uff.br/PosGraduacao/RelTecnicos/385.pdf  (Fast and Safe Prototyping of Game Objects with Dependency)

譯文原文: http://blog.csdn.net/liangneo/article/details/5667442


用依賴注射模式實現快速安全的遊戲對象原型

Erick B. Passos

Media Lab - UFF

Jonhnny Weslley S. Sousa

LCD - UFCG

Giancarlo Nascimento

Media Lab - UFF

Esteban Walter Gonzales Clua

Media Lab - UFF

Lauro Kozovits

UERJ

 

">[摘要]:大多數遊戲引擎是基於遊戲對象的繼承和/或組件化的行爲。雖然這種方法使得系統框架有一個清晰視圖,良好的代碼重用,快速原型化,它帶來了一些問題,主要是遊戲對象/組件的實例的高度依賴。這種依賴性往往導致靜態轉換和很難調試的空指針引用。本文應用依賴注入模式以安全地初始化遊戲對象和減輕在遊戲開發原型和產品發佈階段程序員角色職責。遊戲對象的屬性初始化依存關係注入只發生在初始化階段,而在遊戲循環中,沒有任何性能損失。

[關鍵字] 遊戲引擎架構;依賴注射,對象組合

 

作者聯繫方式:

 

{epassos,esteban}@ic.uff.br [email protected] [email protected] [email protected]


 

1.    引言

在計算機科學領域,很少有像電腦遊戲和麪向對象那樣直接的編程映射關係。一個遊戲類匹配幾個不同的遊戲類的實例存在的虛擬世界概念,遊戲對象可以是任何實物例如一個人物,房屋或者是一個不可見的觸發對象。遊戲執行通常由遊戲類內部具有以下三個目的的循環組成:

 

1.      獲取用戶或網絡輸入,

2.      根據用戶輸入或物理模擬更新遊戲對象,播放動畫和執行AI

3.      在輸出設備上繪製可見的遊戲對象

 

雖然以上的循環形式可以更好地利用並行[鄔斯迪摩賴斯扎米特等。 2007],但我們認爲,此基本模式能很好地反映我們工作的目的。爲表模擬不同類型的對象,程序員通常創建的遊戲對象上的子類,每一個新的遊戲對象指定更專門的內容和行爲。該繼承方法存在的問題是衆所周知的。這方面的一個很好的例子是,當兩個不同層次的對象,有時有共同的功能和特點,導致代碼冗餘。這是在面向對象的軟件設計一個共同的問題因此更換由遊戲引擎繼承爲組合已經爲一個很好的做法[福爾默2007年;斯托伊2006; Ponder2004; Billas 2002]。採用組合替代繼承之後,遊戲對象僅僅只需要擁有共同的屬性如姓名,位置和方向。但最重要的是,它可以作爲一種可重複使用的組件的容器。每一類擴展一個抽象組件描述一個遊戲對象不同的的方面或行爲如物理,AI,或生命值,根據需要來組成遊戲對象。這些組件應該高度靈活,易於維護,同時也最大限度地提高代碼重用,同時這些組件也可以用於不同的遊戲類型。

 

這兩種方法都有一個共同的問題,組件(或遊戲對象)之間的高耦合性。舉個例子來說,一個AI組件通常不但依賴於生命(Health)組件來做出決策同時也依賴於物理組件來運行。一般情況下,這些依賴通常由程序員直接解決。正如以下的java所示(部分AIComponent類,從最初的C++例子[stoy2006]改編)

 

1  public void update(float interpolation)  {

2       final GameObject  o  =  getOwner();

3       Health h  =  (Health) o.getComponent("health");

4       if (h != null)  {

5      // take AI  actions  based on   health

6      }

7  }

 

Code 1: Traditional dependency handling

 

很容易看出此實現(AIComponent)顯示的依賴於生命組件(第3行),該行代碼假定相同的組件對象已經用標識“Health”在遊戲對象中註冊過。如果每個遊戲對象都包含一個AI組件實例並且已經用Health組件初始化過,一切都會像預期的一樣。顯然,這段代碼並沒有什麼不對,但進一步的審視將在下面給出:

 

當有必要清除隱式依賴時,代碼2-4行只是空餘的代碼,與遊戲邏輯的AIupdate無關

代碼段第3行的顯式轉換,或者說在某些腳本語言中濫用強制轉換,是在依賴對象或組件中一個常見的運行時問題

如果一個特定的遊戲對象沒有用Health組件初始化,那麼AI決策將不會被正確執行,使得很難調試。

當設計關卡時,除非除非組件記錄了這些討人厭的依賴,不然程序員還得親自寫一些代碼

 

一段等效UnrealScript代碼[EpicGames 1998]將更加難以調試,因爲該語言通過將指針轉化爲void*隱藏了所有的空指針和引用。Tim Sweeney最近說在Unreal引擎中大約百分之五十的Bug是因爲缺乏強類型檢查[Sweeney 2006].他也指出一個典型的遊戲對象更新通常要涉及到五到十個其它對象,這也顯示了依賴這個關係是多麼的常見和明顯。

本文應用依賴注射(Dependency Injection)模式來解決依賴的部分問題,通過本文,可以減輕程序員手工檢查這些依賴的職責。正如以下章節將給出的一樣,我們的框架解決安全初始化遊戲對象或組件,使得編碼以更加清晰和可維護的方式進行。本文剩下的章節由以下幾個部分組成:第二部分討論相關工作,第三部分描述概念和GCore框架實現的模式;而第四個部分將講述依賴注射模式的使用和我們框架相對於前面研究的優點;最後,第五部分總結本文並概述今後的工作。

2.    相關工作

成功的商業遊戲引擎對遊戲對象繼承有很強的依賴,例如CryEngine[Cry-Tek 2008]使用了類似於遊戲對象和組件相似的實體和實體項。同樣的架構也可以在其它商業引擎中找到,如Unreal Engine[EpicGames 1998]和Torque[GarageGames]。同時也有很多基於組件架構的引擎[UnityTechnologies 2008;Spinor;3DVia;Billias2002]。這些工具的一個共同問題是對象之間的依賴太高,這也是我們研究的潛在目標。在Tim Sweeney[Sweeney 2006]的一次談話中,他暴露了當前編程語言和工具中中的兩個問題細節:較差的並行處理和弱類型檢查。他提出了許多新編程語言應該擁有的特徵,這特徵應該能解決程序員在實現遊戲對象腳本時所遇到的大多數運行時錯誤。他的觀點和我們的多少是有點相似的,但他的目標是創建一門擁有以上特徵的新語言,這並不是一個簡單的任務。在本文中,我們將使用當前流行的技術,這些技術可以應用到大多數工具中。我們的依賴注射實現是基於java的反射機制之上的,這種反射機制已經被作移植到基於如XNA[Microsoft]平臺的C#上。C++和一些腳本 語言是沒有反射機制的,但是花一些時間來設計這樣一系統是可能的[Pocomatic 2007]。Dungeon Siege 是第一批包含完全遊戲對象組件系統之一。在兩界遊戲開發者大會上[Billas2002;Billas2003],Scott Billas 展示組件架構和一些幫助遊戲開發的一些特徵。在最近的交談中[Bilas 2007],他展示了怎樣提高遊戲產品線的想法,它們其中的一些和遊戲對象初始化的健全檢查有關,例如屬性需求和依賴。他提出,這些斷言和和錯誤消息應該直接由組件程序員代替關卡設計師來實現。我們不但認識到這些想法確實非常重要,而且也提出用工具來做健全檢查和依賴注射,代替程序員從而提高整個產品流水線。Haller et al[Haller et al.200]提出了使用通信槽和消息管理來消除強關聯一種新的遊戲對象和組件構架。該解決方案使得組合對象更容易和組件之間聯繫列容易,但是需要一個很複雜的架構。使用我們的方法,程序員根本不用學習新的語言和通信架構,因此這種方法更適合原型開發。Unity3D遊戲引擎[UnityTechnologies 2008] 是一個與其相關的最近產品,它獲得了許多開發者的關注,因爲它有設計得很好的遊戲對象組件系統和場景編輯器,該編輯器還使用了一種可視化的方法來組合對象。在其最新版本中(2.1,在2008,7月下旬發佈),一個簡單的依賴健全檢查形式已經提供給了腳本程序員,腳本程序員可以使用它來說明一個組件依賴於存在的另一個組件,這些都是在運行時在場景編輯器中檢查。然而,儘管是自動的被場景編輯器初始化,這些實體並不是自動的注入依賴組件,而是由腳本程序員來完成,而且腳本程序員還要顯示的調用getComponet(Type)方法來獲取引用。我們的系統既做了健全檢查也做到了自動注入依賴的組件。

據我們所知,所以之前的遊戲對象組件系統研究只走了這麼遠,我們建議全面採用依賴注入來處理遊戲引擎中組件依賴關係。在下面是的章節中,我們的的框架,GCore,將解釋其架構和遊戲對象組合的依賴注射。

 

3.    GCore 框架

圖表 1 GCore組件架構


GCore,即Game Core的縮寫,是一個數據驅動的遊戲框架其目標是使用JMonkeyEngine [JMonkeyEngnie]生產高效遊戲產品,JMonkeEngine是一個用OpenGL實現渲染OpenAL實現音頻的場景圖引擎,包括了一些例如物理引擎[JMEPhysic]和網絡引擎子系統[Imagination]來實現一個可擴展和易用的工具。因爲他的核心概念非常適合用其它平臺和語言如C++,C#一實現,我們把關注點主要放在它的數據驅動和依賴注射功能上,這些功能使得遊戲設計者,關卡設計者程序員和美術師在遊戲產品線上合作更密切。程序員的角色是實現可複用的遊戲組件而關卡設計師則整合組件和美術師作品。在本文中,我們將展示怎樣用GCore工具和技術去幫助程序員和關卡設計師。

3.1       主要概念

從軟件工程的觀點來看,GCore定義了四個主要的概念/類來描述一個遊戲:GameManager,

GameState,GameObject, 和AbstractComponent,GameManager是一個整個系統的外觀模式[Gamma et al,1995]而GameState類似於Use-case圖,每一個State都是一個獨立的遊戲場景(或者其他用戶交互概念)例如3D遊戲場景,菜單或者是HUD,在執行期間,玩家選擇這些場景而遊戲在這些不同的GameState實例之間切換。一個可運行的GameState由一個唯一的名字和一序列遊戲對象(GameObject)組成,每個遊戲對象又由一序列AbstractComponet實現組成。在圖表一中,可以看到GCore的一個簡單的類圖架構,而圖表2則用順序圖通過每一個不同的步驟方法調用用的順序來解釋了GCore中用到的遊戲循環概念。

圖表 2 GCore 遊戲循環


從圖表2可以看出,每一個組件在每一幀頻時都會被更新。在抽象類AbstractComponent中並沒有渲染成員函數,因爲它們中的大多數並不是一個具有圖形屬性的對象。相反,類GameObject維護了一個場景圖結點,在需要時,該結點上的圖形組件可以隨時附加一個可以繪製的幾何圖元。在每一幀結束時,該結點都會被渲染,這樣也使得該附加到該結點上的圖形組件對象渲染到設備上。

3.2       以數據驅的遊戲對象組合

GCore的高效生產率的主要原因是因爲可以完全以一種聲明的形式來創建遊戲。圖表3給出了一個組合遊戲對象的例子,這是一個由兩個GameObject 實例組成的GameState,每一個GameObject又由代表人物我行爲不同組成構成。

在上面的例子中,遊戲狀態中有兩個活動的遊戲對象名字分別爲“npc”和“tree”.Npc對象由一個具有圖形描述的VisualComponent和在遊戲執行中負責控制行爲AIComponent組成。注意到由於Update方法每一幀時都會調用,因此兩個組件都會被更新。在AIComponent中該更新方法包含了實際的AI步驟實現,而VisualComponent的更新方法確爲空。另外一個命名爲“Tree”的對象是靜態的,它僅僅只由單一的含有由幾何圖形的VisualComponent組成。

遊戲狀態和對應的遊戲對象都保存於XML文件之中,這樣不但非常容易維護也適合用於集成例如關卡編輯器這樣的集成開發工具。從代碼段2,可以看到一個簡單遊戲的主要xml配置文件。配置文件的根結點元素是”Game”,該元素有兩個屬性,名字和第一個加載的遊戲狀態。”Game”元素必須由聲明的類型和遊戲狀態組成,爲了使它們能像期望的保持在不同的文件中,通過使用關鍵字“include”提供一個組合徵用類型的方法,而這樣類型又可以在遊戲狀態中作進一步的詳細說明和初始化到遊戲對象。


圖表 3 組合實例


 

<game   name="example" init="menu">

<!-- type definitions  -->

<include file="types.xml"  />

<!-- game   states  -->

<include file="menu.xml" />

<include file="farm.xml" />

</game>

Code 2: Main configuration file (game.xml)

 

類型由許多組件組成,這些組成可以擁有自己的可以在xml文件中說明的屬性值。在代碼段3中,我們可以看出以組合,繼承和屬性說明構成的類型聲明。第一個類型,其名字爲“Basic”,定義了所有的派生類和對象必須有一個VisualComponent附加於其上。第二個類型,其名字爲”NpC”,描述了其繼承於”basic”,因此它不但擁有來自父類的VisualComponent也附加了一個AIComponent。而”tree”類型通過繼承”basic”和修改VisualComponet中的屬性展示了GCore的能力,在這個例子中,我們定義了一個爲任意”tree”類型遊戲對象從外部加載3D模型的作爲幾何數據的描述。

 

<type  name="basic">

<component class="VisualComponent"  />

</type>

<type  name="npc-type" extends="basic">

<component class="AIComponent"  />

</type>

<type name="tree-type" extends="basic">

<component class="VisualComponent">

<model  value="tree.3ds" />

</component>

</type>

Code 3: Sample type declarations (types.xml)

 

遊戲狀態由一些遊戲對象組成,該對象可以從預先定義好的類型和說明派生而來,也可以插入它想要的任意組件。甚至可以定義一個沒基類的對象,但我們並不推薦該方法因爲它不利於代碼重用。在代碼段4展示了圖表3遊戲狀態的xml文件。”npc”對象繼承於預先定義好的”npc-type”並描述了用於VisualComponent的一個3D模型。”tree”對象描述了ViusalComponent一個新的位置向量。我們可以很容易的看出用數據驅動來組合遊戲對象的靈活性,甚至可以讓一個相同的類型對象擁有不同的名字的組件。類GameObject和AbstractComponent是用組合設計模式[Gamma et al.1995]來實現的,在需要的情況下,可以遞歸嵌套組件。

 

<gamestate name="farm">

<!-- game   object1: npc -->

<object name="npc"  type="npc-type">

<component class="VisualComponent">

<model value="zombie.3ds"  />

</component>

</object>

<!-- game   object2: tree -->

<object  name="tree" type="tree-type">

<component class="VisualComponent">

<position  x="10" z="15" />

</component>

</object>

</gamestate>

Code 4: Game state and objects composition (farm.xml)

 

3.3       xml解析和遊戲執行

從上一節可以得出每個遊戲完全由保存在xml文件中的原子數據來說明。該xml配置文件在初始化時解析並加載到一些被配置的對象中。在需要時,輕量級的原子數據結構可以在運行時用於初始化遊戲狀態和遊戲對象。GCore具有解析java所有基本數據和三個值的向量,四元數和例如紋理、模型和聲音文件的能力。當然你也可以通過繼承PropertyParse類解析任意用戶自定義的數據類型。

 

在遊戲中,必須至少具有一個默認的遊戲狀態,該狀態將在最初時被加載。由於遊戲是由一系列這樣的遊戲狀態組成,在運行時,必須用一種方法來初始化,析構和切換它們。爲了提供一個這些特點犀利實現,GameManager類實現了一箇中介模式[Gamma et al.1995].該模式使用了公有方法通過狀態名來激活(重新激活),暫停或者銷燬任意聲明的遊戲狀態。銷燬之前啓用的遊戲狀態是可以選擇的(在激活另一個狀態之前),因爲同上時間在內存中可以存在多個狀態。

 

GameManager類也負責遊戲對象和組件的正確初始化。在初始化遊戲狀態階段,我們使用了生成器設計模式(Builder)[Gamma et al.1995]來初始化遊戲狀態,遊戲對象,組件和屬性。當然也解決了依賴注射。然而,爲了可讀性,我們將在下一節展示一個簡化的版本而不是整個過過程。因爲我們的關注點主要是依賴注射,以及它的優點和實現細節。

 

4 GCore中的依賴注射

在GCore中,所有組件之間的直接依賴都可以用該框架來解決。首先,我們來考慮一下,一個簡單的遊戲對象怎樣能被一些可重用的組件來實現,該遊戲對象由一個外部3D模型,玩家輸入,和跟蹤相機組成。理想情況下,爲了代碼複用性,這些不同的部分將用不同的組件來實現,例如VisualComponent,PlayerInput和ChaseCamera.VisualComponent可以獨立於其它兩個組件用於例如房屋和樹之類的需要一個圖形描述的靜態對象。這些都不需要被一個相機跟蹤或者受用戶輸入控制,因此沒有必要在VisualComponent中包含其它兩個組件。然而,當用於組合一個玩家對象,ChaseCamera和PlayerInput將被使用,並且它們都依賴於幾何數據的方向。它們分別使用該幾何數據作爲觀察目標或者根據用戶命令更新。VisualComponent已經定義了滿足該要求一個幾何屬性(它加載的模型)。圖表4展示了這些對象的關係圖。玩家遊戲對象有一系列的組件,這些組件都是AbstractComponent的子類。


圖表 4 依賴組件實例


圖表4中的組件c2和c3都依賴於組件c1。很明示ChaseCamera和PlayerComponent都有一個VisualComponent類型的屬性,因此它們可以在各自的更新中使用該組件,而不是在更新函數中手動查找該組件,組件程序員只需要包含如代碼段5所示的在運行時可以獲取自定義標記 @Inject到屬性聲明中即可。當初始化每個組件時,正如我們將在下一節解釋的,如果符號@Inject在任意聲明之前找到,我們的框架將在同一個遊戲對象中查找一個該組件類型的實例並設置爲其屬性。

 

class ChaseCamera  extends  AbstractComponent {

@Inject

VisualComponent vc;

public void update(float interpolation){

camera.lookAt(vc.getWorldTranslation());

}

}

Code 5: @Inject in ChaseCamera source code

 

從上面的代碼可以看出,沒有一行代碼是去查找一個VisualComponent並且檢查它是否爲空的。也不難發現,和代碼段1相比,上面的代碼更短,更簡潔和安全因爲在初始化時如果沒有VisualComponent聲明該框架將停止初始化並給出一個錯誤日誌。代碼段6是一個正確的玩家對象組成Xml文件。

 

<object  name="player">

<component class="VisualComponent">

<model value="knight.md5"  />

</component>

<component  class="PlayerInput" />

<component class="ChaseCamera"  />

</object>

Code 6: Correct player object composition


 

圖表 5 遊戲對象初始化


由於聲明中包含了一個VisualComponent,和其它兩個依賴該組件的組件,初始化可以正常進行,遊戲對象的初始化順序如圖表5所示

從圖表5中可以看出,當創建遊戲狀態時,所有的遊戲對象和組件及它們的聲明屬性將首先被初始化。經過第一個步驟(圖表中消息2,3和4),遊戲對象被正確的初始化,組件的依賴也被注入(消息5和6)。最後,通過依賴注入,所有聲明的組件都被附加到遊戲對象上。也使得所有的依賴得到解決。

 

在代碼段7中,描述了一個不正確的玩家對象組成。讓我們想象一下,例如一個頭上設計師犯了一個錯誤,他認爲“character”的父類已經擁有了一個VisualComponent,僅僅只包含了另外兩個所需的組件。因爲這樣的描述是不合法的,我們的框架將在初始化時給出一個如代碼段8所示的錯誤而不會導致一個未知的運行時錯誤。

 

<object  name="player" type="character">

<component  class="PlayerInput" />

<component class="ChaseCamera"  />

</object>

Code 7: Incorrect player object composition

 

 

"Incomplete composition of object: ’player’.

Missing required  ’VisualComponent’

needed by   included  ’ChaseCamera’."

Code 8: Unsolved dependency initialization error

 

通過自動處理組件之間的耦合和安全的初始化遊戲對象,程序員將不再需要手動來檢查顯示的依賴和空引用。從上面的例子可以得出:由於更少代碼調試的依賴,遊戲產品線中的關卡設計得到了提高。

 

4.2 實例2:遊戲機制的快速原型

在上面的介紹中,我們指出由對象和組件之間依賴所造成問題之一是許多代碼被用來處理與遊戲邏輯無關問題。像這樣的樣板代碼會耗費程序員大量的時間,通常,調試這些代碼也會耗費很多時間。在該實例中,我將展示如果用依賴注射通過使程序員只集中核心機制的實現來提高組件快速原型技術。

 

假設我們將要實現一個“月球貨船”遊戲,該遊戲的核心機制由控制一個火箭助推的重型運輸工具組成。實現的目標是儘快的暴露核心機制給早期的的測試人員。以下幾點是這個原型的重要特點:

 

1.      有一個類似於月球的地形

2.      重力和物理碰撞系統

3.      3D模型加載

4.      推動登月倉的玩家控制(這是最重要的一點)

 

很容易看出特點1-3在其它遊戲系統中也會用到,而且在GCore中這些特點已經被開發爲可複用的組件了,因此只需要實現最後一個玩家控制推動器即可。在代碼段9中,我們可以看到用於該原型的xml文件,該xml文件聲明瞭一個由地形和登月倉遊戲對象組成的遊戲狀態,推動器類的實現將在下一部分給出。

 

<game   name="lunarCargo" init="moon">

<gamestate  name="moon">

<!-- prototype object1: terrain  -->

<object  name="terrain">

<component  class="TerrainComponent">

<heightmap value="moon.png"  />

</component>

<component  class="TerrainPhysics" />

</object>

<!-- prototype object2: lunar module  -->

<object  name="module">

<component class="VisualComponent">

<model  value="cargo-ship.3ds" />

</component>

<component  class="DynamicPhysics" />

<component  class="Thrust" />

</object>

<gamestate>

<game>

Code 9: Lunar Cargo prototype XML

 

GCore的DynamicPhysics組件依賴於VisualComponent,因爲前者不但要使用它作爲碰撞的幾何數據還要在物理模擬時移動它。很明顯推動器的實現也依賴於DynamicPhysics,因爲當用戶輸入“推動”時,DynamicPhysics必須應用力到推動器上。代碼段10展示了推動器類的實現,推動器對DynamicPhysics的依賴通過符號說明@Inject暴露給框架,更新(Update)方法在行動時默認該屬性值是非空的。

 

很明顯程序員可以更集中精力於核心機制的實現上:檢查用戶的輸入並將其實施於登月倉推動器的物理系統上。通過這種方法,我們相信人們可以在遊戲開發流程中寫出更好的代碼和獲得更快的原型又不用花很多時間在設計上。

 

5小結

數據驅動被證實爲在遊戲開發流程中管理風險的好方法。正如許多成功的遊戲引擎和架構展示的一樣,將這些與一個良好設計的遊戲引擎結合起來,將會得到一個強大的框架。然而,面向對象,特別是組件組合,有許多維護性和高度依賴的問題。在本文中,我們描述了一個基於依賴注射的方法安全的將這些職責從程序員中移除。

 

衆所周知,在遊戲開發週期中,基本上不可能同時寫個良好設計代碼並且實現快速原型。我們堅信通過使用GCore可以實現快速原型並且不用放棄良好的編程實踐。通過使用依賴注射,我們完全移除了在遊戲對象腳本中是十分常見的隱式依賴硬編碼。GCore組件庫,提供了一個強大的,可擴展的和安全的工具。

 

GCore是一個正在進行的長期工作,我們現在正研究怎樣將這些技術用到其它場合例如組件之間依賴和從其它遊戲對象和組件獲取屬性的依賴的遊戲設計。我們計劃擴展這些符號的使用以至於程序員可以應用如not-aull,mix/max values/length這樣的約束到任意基礎類型(附加文件,字符串,向量和四元數)。我們短期計劃也包括了一個提供可視化組合的關卡編輯,這樣關卡設計師就不用親自寫xml說明文件。

 

致謝

我們要感謝Scott Bilas對遊戲的對象組件的依賴和其他相關的軟件工程和目前在遊戲引擎的發展趨勢有關問題的有意義的討論。

 

我們也非常感謝在JMonkeyEngine討論論壇的每個人,特別是發開發者,總是願意幫助解決渲染,音頻和物理等問題,你們的幫助才使得GCore框架成爲可能。


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