2010年4月28日

,

Component-Based Game Object System, Part I

前言

在一些較舊的Game Engine裡,遊戲裡的各種物件是以繼承 (inheritance) 的方式來重複使用 (reuse) 程式碼,例如左圖是一例射擊遊戲很常見到的繼承樹 (inheritance tree)。

fig1

隨著遊戲程式發展地越來越大,繼承樹也會越來越深,最後程式變得又笨又重而難以維護。以左圖為例,如果企劃人員希望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 的架構。

介紹

fig2

Component-based 的遊戲架構會像右圖一樣。我們將遊戲物件的各種功能肢解成許多較小架的類別,也就是 Component,而 GameObject 則純粹是 Component 的容器 (Container) 。比如以 CMonster 為例,它所繼承的 CRagDoll 變成 RagDollComponent,CMesh 變成MeshComponent,CPhysics 變成 PhysicsComponent。如果我們要再為 CMonster 加上 AI 和音效,則再 GameObject 內加入 AiComponent 和 SoundComponent。

fig3

  於是原本很深的繼承樹,經過 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...


參考



  1. Chris Stoy, Game Object Component System, in the book Game Programming Gems 6
  2. http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
  3. http://www.gamedev.net/community/forums/forum.asp?forum_id=11
Publisher: Unknown - 星期三, 4月 28, 2010

2010年4月9日

,

This is Game Industry, This is Taiwan

我的夢想是寫個好程式,做個好遊戲。但在這個工業,似乎是永遠沒有足夠的資源讓你做「好」一個遊戲。

看著對手,K國的T公司,做出來的產品,既豐富又有趣,一個接一個推上架。我不禁思考:為什麼他們可以又快又好,而我們的品質與內容總得在對方的強勢下求生存。據說,他們是做了又長又久,研發了好幾代,人又多。而這裡,受限於有限的資金,我們必需在及短的時間,很少的人力把遊戲生出來,然後得到限金和利潤。所以,我們可以說,輸了是很合理的嗎?資方或許期待只以5個[人x年]生出來的遊戲,就能和100[人x年]生出來的遊戲平起平坐,我們身為沒有投入資金的勞方而言,有資格抱怨嗎?我們能夠說:「資方TMD投入一個工程師二位美術一個企劃才8個月就肖想能做出像X遊戲一樣賣座的東西!」嗎?究竟是資方太貪婪,還是勞方太怠惰?

我想不負眾望做出好遊戲,所以當感到時程壓力,連續一個月,下班也把程式當晚餐回家寫,周末也花了一半的時間在家工作。很快地,就疲乏了。同事勉勵我說,公司看的是你付出多少,付出多的,以後有賺錢回饋也多。他的勉勵在我腦中回響,我深思。他的意思是有賺錢,你有賣命就有錢分。但賺錢前提是有客戶要買你的遊戲,然而,要客戶買你的遊戲,也要遊戲夠「好」。

我能夠做「好」遊戲,只要給我報酬和時間,報酬能夠延續「以做遊戲為職志熱情」,時間(泛指人力乘以天數)是始於成就產品不可超越的物理極限。報酬可以寄托未來,而不要求,但後者無論卻如何不能缺乏。所以在資方不願付出時間的情況下,只憑勞方打拼真的能做出「好」的遊戲嗎?能做出賺錢的遊戲嗎?我給個很大的問號。或許接受了同事的勉勵後,到頭來還是因為沒有時間而失敗了,遊戲夢最後還是一場空。

我想這是一個惡性循環,資方越不願意投入耐心和時間,產品就愈可能失敗,而產品愈可能失敗,資方就愈不願意投入時間給勞方的報酬也愈低。要打破這循環,我想不能期望短視保守的資方承擔風險,畢竟它是商人,所以在這個產業永遠是對遊戲有熱情的勞方先付出,先承擔風險。你也許會問什麼風險?勞方付出了歲月、健康和生活,期望有一天自己熱愛的事物會帶來豐厚的報酬,如果失敗了,沒了活力,也沒有其它領域的經歷,前份工作的薪水又如此之低,下場我想是很淒涼吧。

我們,遊戲人生的追隨者,是不是該拿起尚未失去光環但保存期限快過的畢業證書,趁我們的劍刃還沒磨頓,去找個外商,圖個起碼在另一半和親人面前走路有風的月薪?寫到最後,被自己的文字弄得頹喪。雖然,我心中仍有一絲的希望,我想再寫幾個句子來個積極上進的結尾,但我發現我無法,彷彿是把那微弱的光明說出來,它就煙消雲散。

Publisher: Unknown - 星期五, 4月 09, 2010

2010年4月3日

,

RT Shader System in OGRE 1.7.0

前言

OGRE在1.7.0版中,最重要的feature之一就是real-time shader system (RT Shader System),它的功能就是從OGRE的material script中自動產生shader (HLSL/CG或GLSL檔案)來取代或增加原本fix pipeline的功能。 使用RT Shader System好處呢?

  1. 美術和程式分工

    以往要要在Ogre中使用normal map必需大費周張的寫vertex shader和pixel shader再把全域變數(global variable)關連到OGRE裡的光源(lights)和座標轉換(transforms),在團隊裡程式人員和美術人員往往是不同一群人,因此新增新的美術資料,就必需勞動程式再編輯material script,分工不完全使得團隊效率降低。

  2. 開發快速

    因為shader是從shader範本(template)中產生出來,而範本函蓋大部分的繁覆的工作,所以開發人員能過集中精力去處理shader裡較重要的部份(藉由撰寫RT Shader System的plugin),使得實作新的shader更快速。而撰寫好的plugin又能在material script透過參數使用,使得整合也更快速。

使用

  1. 設罝開發環境

    使用RT Shader System需先在程式專案中連結OgreRTShaderSystem.lib (Debug組態是OgreRTShaderSystem_d.lib),放在OgreSDK\lib中。並且在初始化OGRE和RT Shader System的cpp檔include RT Shader System的header:

    #include "OGRE/RTShaderSystem/OgreRTShaderSystem.h"


  2. 初始化

    RT Shader System必須早於任何一個Resource Group初始化,通常是在產生第一個RenderWindow之後。

    RenderWindow* window = Root::getSingleton().initialise(true, windowTitle);
    //在這裡初始化RT Shader System
    _initializeRTShaderSystem();

    ResourceGroupManager::getSingleton().initialiseAllResourceGroups();

    以下是_initializeRTShaderSystem()的definition

    bool _initializeRTShaderSystem()
    {
    if (RTShader::ShaderGenerator::initialize())
    {
    //先取得ShaderGenerator的pointer
    _shaderGenerator =
    RTShader::ShaderGenerator::getSingletonPtr();
    //設定ShaderSystem所使用的Shader語言
    _shaderGenerator->setTargetLanguage("hlsl");
    //你可以指定一個目錄來暫存RT Shader System產生的Shader,
    //這樣就不需要重覆產生Shader
    //但1.7.0的RT Shader System只接受絕對路徑,
    //因此此處使用boost::filesystem把相對路徑轉換成絕對路徑
    namespace fs = boost::filesystem;
    std::string shaderCachePath =
    fs::system_complete( fs::path("./ShaderCache/") ).string();
    _shaderGenerator->setShaderCachePath(shaderCachePath);

    //指定一個material listener給material manager,此處稍後說明
    _rtssMaterialListener =
    new RtShaderSystemListener(_shaderGenerator);
    MaterialManager::getSingleton().addListener(
    _rtssMaterialListener);
    return true;
    }
    return false;
    }

    RtShaderSystemListener繼承MaterialManager::Listener,是從OGRE Sample裡抄出來的class。

    /**
    當RtShaderSystemListener被註冊給MaterialManager之後,一旦找不到適當的tecnique
    來render 某一個material,便會喚起handleSchemeNotFound這個member function。因
    此我們要在handleSchemeNotFound把原本的material technique交給ShaderGenerator
    來產生以Shader為基礎的tecnique並回傳。
    */
    class RtShaderSystemListener : public MaterialManager::Listener
    {
    public:

    RtShaderSystemListener(RTShader::ShaderGenerator* pShaderGenerator)
    {
    mShaderGenerator = pShaderGenerator;
    }

    /**
    當Entity要被render時,OGRE會依指定給viewport的scheme name從material挑選符合
    的technique(在我們清況中,會指定ShaderGenerator::DEFAULT_SCHEME_NAME給
    viewport)因為material裡tecnique的scheme name預設是
    MaterialManager::DEFAULT_SCHEME_NAME,
    所以handleSchemeNotFound便會被呼叫來處理"找不到technique"事件。
    */

    virtual Technique* handleSchemeNotFound(
    unsigned short schemeIndex,
    const String& schemeName,
    Material* originalMaterial,
    unsigned short lodIndex,
    const Renderable* rend)
    {
    Technique* generatedTech = NULL;

    //
    //Case this is the default shader generator scheme.
    if (schemeName == RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME)
    {
    bool techniqueCreated;

    // Create shader generated technique for this material.
    techniqueCreated = mShaderGenerator->createShaderBasedTechnique(
    originalMaterial->getName(),
    MaterialManager::DEFAULT_SCHEME_NAME,
    schemeName);

    // Case technique registration succeeded.
    if (techniqueCreated)
    {
    // Force creating the shaders for the generated technique.
    mShaderGenerator->validateMaterial(schemeName,
    originalMaterial->getName());

    // Grab the generated technique.
    Material::TechniqueIterator itTech =
    originalMaterial->getTechniqueIterator();

    while (itTech.hasMoreElements())
    {
    Technique* curTech = itTech.getNext();

    if (curTech->getSchemeName() == schemeName)
    {
    generatedTech = curTech;
    break;
    }
    }
    }
    }
    return generatedTech;
    }
    protected:
    /// The shader generator instance.
    Ogre::RTShader::ShaderGenerator* mShaderGenerator;
    };
    最後,我們必需要讓OGRE使用RT Shader System所產生的technique(亦即以shader為基礎的technique)。因此我們設定viewport所要使用的scheme name。
    viewport->setMaterialScheme(
    RTShader::ShaderGenerator::DEFAULT_SCHEME_NAME);
Publisher: Unknown - 星期六, 4月 03, 2010