前言
在一些較舊的Game Engine裡,遊戲裡的各種物件是以繼承 (inheritance) 的方式來重複使用 (reuse) 程式碼,例如左圖是一例射擊遊戲很常見到的繼承樹 (inheritance tree)。
隨著遊戲程式發展地越來越大,繼承樹也會越來越深,最後程式變得又笨又重而難以維護。以左圖為例,如果企劃人員希望CMachineGun能有animation的功能,以做出格林機槍旋轉槍管的動畫,這時CWeapon就要從繼承 CStaticObject 改成繼承 CAnimable,連帶的 CWeapon 和 CWeapon 的所有子類別 (derived class) 都要重新翻修,這對時間一向不夠用的遊戲產業是個大麻煩。
會出現以上問題,主要是因為這幾年來軟體設計工程師對「繼承」本身的誤用。在 Effective C++ 中曾說明父類別和子類別必須要有 is-a 的關系,而 is-a 意味著子類別應具有父類別所有的功能,但實際上大部份使用繼承的程式碼都不符合這個定義。比如 CPhysics 本身可能有100個 function,但使用 CMonster 的code,只會用到 1/10 的 CPhysics 的 public member function,甚至以 CMonster 的實體呼叫繼承而來的 CPhysics 的 public member function 還會造成 CMonster 的程式碼產生執行期錯誤。
事實上,以程式而言如果我們說:「CMonster is a CPhysics」(CMonster是一種CPhysics) 是一種是似而非的判斷,CMonster 實際上是使用 (use) CPhysics,CMonster 該擁有 (has-a) CPhysics 這個元件(component),也擁有CMesh這個元件。同理大部份的 has-a 很容易就被誤解成 is-a 所以造成該用 composition/aggregation 卻誤用成inheritance 而產生無法管理的巨大繼承樹。這也是 component-based 的設計誕生的原因:強迫programmer使用aggregation。當然 component-based 的設計還有很多優點:易於分工、快速地做出物件原型 (fast prototyping) …等,因此多數的商用引擎都紛紛把原本的繼承樹改寫成 component-based 的架構。
介紹
Component-based 的遊戲架構會像右圖一樣。我們將遊戲物件的各種功能肢解成許多較小架的類別,也就是 Component,而 GameObject 則純粹是 Component 的容器 (Container) 。比如以 CMonster 為例,它所繼承的 CRagDoll 變成 RagDollComponent,CMesh 變成MeshComponent,CPhysics 變成 PhysicsComponent。如果我們要再為 CMonster 加上 AI 和音效,則再 GameObject 內加入 AiComponent 和 SoundComponent。
於是原本很深的繼承樹,經過 Component-Based 的方法重構之後,便形成如左圖般淺薄。而且更棒的是,負責AI、Rendering、和物理引擎的工程師可以在不同的類別裡工作,大大地減低共同工作 (cooperative work) 的難度。如果我們想增加新的一種 GameObject,只要根據該 GameObject在遊戲裡的行為 (behavior) 為它加入所需的 Component 就行了,因此開發團隊可以快速實驗各種不同的 GameObject 而不需翻修大量的程式碼 (fast prototyping)。
以下是 GameObject 概略的定義 (細節省略):
class GameObject
{
public:
///
GameObject( const string & name, GameScene* scene );
///
~GameObject();
/// 新增一個 Component
template< typename T > T* addComponent()
/// Component 的數目
int getNumComponents();
/// 取得 Component
template< typename T> T* queryComponent();
private:
/// Component 的容器,key是Component的id,可能是字串或整數
typedef std::tr1::hash_map<Component::id_type, Component*> _components;
};
而Component的概略的定義如下 (細節省略):
class Component
{
public:
///
typedef std::string id_type;
///
Component(GameObject* owner);
///
virtual void initialize();
///
virtual ~Component(){}
/// get component id
virtual id_type getComponentID() = 0;
/// framely update
virtual void update( float timeElapsed ) = 0;
};
Component間的溝通
便利不是沒有代價,工程科學界裡常說的 trade-off 就在此時得到印證。Component-based 設計最大的問題就在 Component 之間溝通的方式。以下是四種常見方式,每一種都有優缺點,有時會合併使用。這裡會先討論第一種,其它就留到 Part II 吧:
- 直接引用 (Direct reference)
- 訊息傳遞 (Message passing)
- 公開屬性 (Property exposing)
- 訊號/接口機制 (Signal and slot mechanism)
直接引用 (Direct reference)
在 Game Programming Gems 6 裡的文章 4.6 Game Object Component System 所使用的方法就是直接引用,它是指透過 GameObject 直接取得其它Component的pointer並直接呼叫其它Component的function。以動作遊戲常見的物理系統和繪圖系統之間的溝通舉例:PhysicsComponent 必需每個frame更新MeshComponent的做標,以下是程式碼範例。
void PhysicsComponent::initialize()
{
_meshComponent = _owner->queryComponent<MeshComponent>();
}
void PhysicsComponent::update()
{
_meshComponent->_setTransform( _fetchPhysicsTransform() );
}
直接引用的優點就是速度快,直接用一個 ember function call 就完成了溝通,然而卻造成 Component 間的高度耦合 (tight coupling) 。PhysicsComponent 只向 MeshComponent 提供 Transform ,當我們要設計新的 GameObject ,希望 PhysicsComponent 能更新 Transform 到 ParticleComponent,我們便必須撰寫 PhyiscsComponent 的第二個版本 PhysicsComponentForParticle。當然,這麼做的話會大大降低的系統的延展性,和團隊合作分工的能力。因此,其它三種方法順應而生。
To be continued...
參考
- Chris Stoy, Game Object Component System, in the book Game Programming Gems 6
- http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
- http://www.gamedev.net/community/forums/forum.asp?forum_id=11
我找不到part II QQ
回覆刪除志遠哥我太晚看到了 現在的CWEAPON IS A CHARACTER
回覆刪除請問 Component間的溝通 其他三種方法哪編有資料可以參考??
回覆刪除