遊戲程式的設計模式  

Posted by 藍斯洛 in

前幾天,老闆找我討論一個問題:要給程式新人怎麼樣的訓練,才能讓他們在專案開發上發揮該有的戰力?

我把這個問題繞了兩個彎,換成:有什麼東西是新人不懂,卻深植在老手腦子裡的?

於是我找到了幾個重點,其中一個就是遊戲程式的設計模式。

四人幫的書裡面所寫的設計模式,我稱它做一般化的設計模式。當然,在遊戲程式裡,也是多多少少會用到個幾種模式的,像是Singleton、Factory、State之類。

而我們要再進一步做整理的,是在遊戲程式裡常用的設計。

例如,在遊戲資源或是繪圖資源的管理上,運用Reference Count很常見,但是對剛跨入這一行的程式新人來說,Reference Count可是連聽都沒聽說過。而另外一個極端的資源管理方式,Garbage Collection,又是怎麼回事?

程式老手會說,拜託! 這是基本觀念好不好?

不過,要讓新手程式自己搞懂這是什麼,可是一個不算低的門檻。

以往,我們用的是,師父領進門,然後自己修行,從數十萬行的程式碼裡頭,自己把這些所謂的「基本觀念」搞懂的方式來訓練新人,訓練期不但太長,也未必人人都有辦法自己「頓悟」出來。而且,如果急就章似的就這樣把新人丟上線去打仗,那是很容易出問題的。例如,Reference Count一旦用錯了,小則memory leak,大則crash。

反正都不是好結果。

然後我們為了減少出錯的機率,將Reference Count的機制,套上了Share Pointer的Template來使用....

這又是新手程式要跨過的另一個門檻。

老手知道Reference Count的使用時機、使用方式,以及設計上所要考慮的點,也知道Share Pointer究竟要防範什麼樣的錯誤,以及會帶來什麼麻煩。但對新手程式來說,這是:千頭萬緒,治絲益棼。

所以我們要拉出一條可以當作起點的線頭,將這些遊戲程式中的設計模式,從複雜的程式碼裡頭整理簡化出來,降低新手程式的進入門檻。

也許哪一天我可以把某些個整理出來的模式Po出來也說不定......

Material Script and Technique  

Posted by 藍斯洛 in

我想,大概沒有什麼繪圖或遊戲引擎不在引擎裡頭使用Material Script。

不過今天的重點不是 Material 怎麼設計 ( DirectX 裡頭的 Effect file 已經算是一個不錯的設計了 ),而是在使用 Material Script 所遇到的問題。

舉個例子,我們在遊戲裡,點選一個物品或怪物的時候,物品會有特殊的標示,像是加個邊框或是變得比較亮什麼的。因為Render的方式不同,所以我們必須更換不同的Render State或是Shader。

直覺的方法是,把Material Script換掉。直接換成加邊框的Material。

不過這樣會衍生一些問題。

第一個問題是,我們在遊戲裡有很多種Material,是不是每種Material都要再加一套加邊框的格式? 而且呢,更換 Material Script 意謂著我們需要重新編譯 Script,重新編譯 Shader,重新設定 Script 中的參數。同時,我們必須訂出一套 Material Script 檔案的命名規則,否則我們怎麼找到正確適合的 Material Script ?

另外一個問題。在我們的設計裡,Material Script 是屬於共用的檔案資源,同一個 Material 可以因為模型的不同而指定使用不同的貼圖,甚至於同一個模型都可以任意的更換貼圖。所以這麼一來,貼圖成了額外設定的一個變數,而這個變數,必須在更換 Material Script 時保持不變。

所以我沒有採用這種方法。

我的 Material Script 是直接使用 DirectX Effect file 的,Effect 裡頭有支援 Multi-Technique 的結構,所以我就修改了引擎中的 Effect Material 系統,將 Multi-Technique 的架構加了進去。

於是呢,我的 Material Script 中,除了預設的Technique之外,又加了一個加邊框的 Technique,需要更換 Render 方法或 Shader 的時候,就切換指定的 Technique,這麼一來,不但可以快速切換,不需要特別的檔案命名規則,而且,像是光源、貼圖等等在 Render 時所需要的參數,也不需要重新再連結設定一次。

不過,壞處是,我必須在每一個有需要更換 Technique 的 Material Script 中,把這樣的 Technique 都加進去,也是不小的工程。

還是說,誰有更簡潔的解決方法?

BattleStar Galactica看完了  

Posted by 藍斯洛 in

  image

image

(圖片來自BattleStar官網)

終於,把BattleStar給看完了。

每次看完一季的BattleStar,都會想把EVE Online的帳號再去開起來玩玩,這次也不例外。

雖然說,這最後一季第四季的劇情有點扯(其實從第三季開始就感覺編劇在掰了),中間還碰到一次美國編劇罷工,造成劇情中間有一個段落,不過,還是很好看的啊!

小時候看過所謂的青少年版的BattleStar,那時候最崇拜的角色就是Starbuck,技術最好,最帥的飛行員。現在長大啦,看的又是比較晦暗,成熟版的BattleStar,可是最喜歡的角色還是這裡頭的Starbuck--雖然在這個版本裡被改成了女的。

BattleStar的背景設定是很有趣的,撇開那個被自己製作的機器人追殺不談,就說這個太空艦隊吧。艦隊不光是有軍艦跟戰鬥機而已,還有一大堆平民所搭乘的太空船,提煉燃料的船、運輸囚犯用的船、殖民地流亡政府的殖民地一號、上頭有五星級飯店跟賭場的星雲九號、還有很重要的一艘資源回收垃圾船....

比起其他的太空科幻片,這樣的艦隊設定才是合理又完整。

哪一天我們要設計這類型科幻遊戲的時候,一定要有這樣的設定啊。

話說,第四季劇情走到一半,Starbuck帶隊找到了地球,不過,這個地球已經是一片殘破,只留下核戰之後的廢墟。然後就看美國的編劇們要罷工多久了。

幸好,第四季又拍了後面十集。

整個艦隊放棄了這個核戰廢墟,繼續找另外一個可以居住的星球。經過一場沒成功的軍事政變,死的死,傷的傷,在大結局的時候,找到了一顆有生命可以居住的星球,也就是十五萬年前的我們這顆星球,艾達瑪指揮官就稱它做「地球」。

哈。

所以我們的文明是Starbuck他們帶來的,我們的血統裡頭有一部分是Cylon複製人....

執行緒函式的設計  

Posted by 藍斯洛 in ,

我在引擎的設計裡,修修改改的弄出了幾種執行緒函式的架構。

一開始是一個thread loop,執行緒函式中有一個無窮迴圈,像是我在Render Thread做的這樣,

DWORD WINAPI CRenderThread::RenderThreadFunc(LPVOID thread_obj)
{
    CRenderThread* thread_ptr=(CRenderThread*)(thread_obj);

    while (!s_bExitThread)
    {
        CRenderCommand* cmd=0;
        thread_ptr->m_CommandQueueLock.Lock();
        if (thread_ptr->m_CommandQueue.size())
        {
            cmd=thread_ptr->m_CommandQueue.front();
            thread_ptr->m_CommandQueue.pop_front();
        }
        thread_ptr->m_CommandQueueLock.Unlock();
        if (cmd)
        {
            cmd->Execute(thread_ptr->m_pRenderThreadState);
            medelete cmd;
        }

        Sleep(0);  // 這個很重要,如果不休息,主緒會忽快忽慢,而且可能兩次執行的速度差異幾十倍
    }
    return 1;
}

迴圈的最後,要讓執行緒睡一下下,因為如果不這麼做的話,在Windows的架構裡,這個執行緒可是會吃掉CPU不肯放出來的,也就會造成主緒所分配到的時間忽多忽少,跑得忽快忽慢。

另一種執行緒函式,是用在背景讀檔的執行緒,每個執行緒只執行一次。

DWORD WINAPI CThreadStream::LoadingThreadFunc(LPVOID thread_obj)
{
    CThreadStream* thread_ptr=(CThreadStream*)(thread_obj); 

    WaitForSingleObject(thread_ptr->m_BeginEvent,INFINITE);  // wait for begin 
    thread_ptr->Load(thread_ptr->m_szRequestFilename,
      thread_ptr->m_szRequestPathID);
    SetEvent(thread_ptr->m_EndEvent);  // loading done
    return 1;
}

執行緒建立之後,並不會馬上進行讀檔,而是會等待一個開始的事件,讀檔完成以後,再設定結束事件,讓主緒可以得知檔案已經讀完。

這個設計,有個問題。也就是每讀取一個檔案,我們就必須建立一個執行緒,讀完了,就刪除執行緒。這在系統資源的使用上,還有執行的效能上,都不是很好的設計方式。

我沒有用Thread Pool來改進,而是做了其他方向的修改。

最先是用了一個無窮迴圈來做。

DWORD WINAPI CThreadStream::LoadingThreadFunc(LPVOID thread_obj)
{
    CThreadStream* thread_ptr=(CThreadStream*)(thread_obj); 
    
    while (!s_bExitThread)
    {
        if (WaitForSingleObject(thread_ptr->m_BeginEvent,0)!=WAIT_OBJECT_0)
        {
            Sleep(0);
            continue;
        }
        thread_ptr->Load(thread_ptr->m_szRequestFilename,
                                      thread_ptr->m_szRequestPathID);

        SetEvent(thread_ptr->m_EndEvent);  // loading done
        ResetEvent(thread_ptr->m_BeginEvent);  // reset begin for next loop
    }
    return 1;
}

這個迴圈每次都檢查開始事件是否被設定,如果沒有,就讓執行緒睡一下下,然後繼續迴圈檢查。用這個方法,我可以讓背景讀檔的執行緒一直活著,有需要的時候,就幫忙讀個檔案。

但是這個執行緒的CPU負擔其實還是很重的,要一直looping檢查。而讀檔的工作,執行的頻率又不是很高。一直looping非常不划算。

所以我將開始事件改成了無限等待,同時加上一個離開執行緒的事件旗標。

DWORD WINAPI CThreadStream::LoadingThreadFunc(LPVOID thread_obj)
{
    CThreadStream* thread_ptr=(CThreadStream*)(thread_obj);

    HANDLE hWaitHandle[2]={ thread_ptr->m_BeginEvent,thread_ptr->m_QuitEvent };
    while (TRUE)
    {
        DWORD signal=WaitForMultipleObjects(2,hWaitHandle,FALSE,INFINITE);  // wait for begin or quit
        if (signal==WAIT_OBJECT_0+1) break;

        thread_ptr->Load(thread_ptr->m_szRequestFilename,
       thread_ptr->m_szRequestPathID);

        SetEvent(thread_ptr->m_EndEvent);  // loading done
        ResetEvent(thread_ptr->m_BeginEvent);  // reset begin for next loop
    }
    return 1;
}

這樣一來,這個執行緒只會在開始事件被設定時才會醒來工作,做完又會繼續等待。

雖然不算是最佳的設計,倒也符合我們現在的需求了。

What Went Wrong? Learning From Past Postmortems  

Posted by 藍斯洛 in

這是今天在Gamasutra看到的文章。

What Went Wrong? Learning From Past Postmortems

by Brandon Sheffield

=============================================================

作者將過去三年發表在Game Developer Magzine上的各個遊戲的Postmortems,從它們所列出的錯誤中,整理出十大錯誤。(通常出現了五次以上)

1. Content added too late.

( 這事不稀奇,我們開發的時候,可是隨時隨地都在加內容的呢! )

2. Communication.

( 溝通? 有嗎? 有嗎? )

3. Scope and scale.

( "野心是兩面刃","去啃咬超過你所能吞下的東西,就意味著你的專案會反咬你一口" )

4. Hiring.

( 人才難找,好的人才與適合的人才更難找 )

5. Juggling projects, lack of leads.

( 意思是說,你將人力在專案之間像拋球雜耍般的拋來拋去,沒有主程式人員,主美術人員,也是一種大問題 )

6. Lack of technical documentation.

( 技術文件是很重要,但是程式人員最討厭寫文件!! )

7. Outsourcing.

( 外包也是一個專案風險 )

8. Polish.

( 遊戲要上市之前,請先把它擦亮,讓它符合玩家的期望,把細節做好。 )

9. Poor tool implementation.

( 工具程式做不好,也是會影響成敗的 )

10. Crunch.

( 這個我就真的看不懂了,到底是指財務面的吃緊,還是時間吃緊? )

好了,這十個大錯誤,我們犯過幾個?

全部。

一個都不少。

Scene Graph Traveler  

Posted by 藍斯洛 in

這一篇,就當做給我自己的記錄吧。

Scene Graph Traveler是我自己取的物件名字,是一個獲准在Scene Graph的樹狀結構中逛大街的物件。之所以會設計這樣的物件,主要是要從一個最近冒出來的需求,來調整整個引擎在Scene Graph中的架構。

原本,我在引擎中有一個Frustum Culler物件,負責將Scene Graph中所有可被Frustum見到的節點蒐集起來,然後一一交給Renderer做繪製。Culler會以遞迴呼叫的方式,跑一遍Scene Graph的樹狀結構,檢查與節點的碰撞關係,如果節點已經被Culler剔除了,那麼節點的Sub-Tree就可以不必再計算。

Scene Graph的遞迴方式,就類似以下的Code

er_code CNode::OnGetVisibleSet(CCuller* culler)
{
    if (node in frustum)
    {
        culler->Insert(this);
     }
     else return ER_OK;

    if (m_ChildList.size()==0) return ER_OK;
    er_code er=ER_OK;
    ChildList::iterator iter=m_ChildList.begin();
    while (iter!=m_ChildList.end())
    {
        er=(*iter)->OnGetVisibleSet(culler);
        if (er) return er; 
        ++iter;
    }
    return ER_OK;
}

 

現在,需求來了。

我要用滑鼠指標點選場景裡面的一個物件,所以我將滑鼠的座標,轉換成場景空間中的一條射線。我叫它做Picker。

Picker搜尋物件的方式跟Culler很類似,也是需要跑一遍Scene Graph中的樹狀結構,檢查射線與節點的碰撞關係,如果射線與節點有碰撞,就繼續搜尋Sub-Tree,否則就可以不必再計算。

簡單的做法呢,就是Scene Graph中,再增加一個讓Picker遞迴的函式。不過這麼一來,我就讓Picker與Scene Graph綁在了一起。

所以這絕對不是好設計。

Culler與Picker在Scene Graph中的行為幾乎是相同的,所差別只在判斷是否要繼續搜尋Sub-Tree的方式。至於在樹狀結構中遞迴的方式則是完全相同。所以我將這行為抽離出來,變成了Scene Graph Traveler。

class CSceneTraveler
{
public:
    enum TravelResult
    {
        Travel_InterruptAll=0,
        Travel_Interrupt,
        Travel_SubTree,
    };
public:
    CSceneTraveler() {};
    virtual ~CSceneTraveler() {};
    virtual TravelResult TravelTo(CNode* ) =0;
};

這是一個抽象物件,需要在Scene Graph中逛大街的物件,都可以繼承它來取得在Scene Graph中遞迴的許可。所以,Culler以及Picker就從這個類別繼承出來,並且各自實作所需要的TravelTo函式。

而Scene Graph Node本身的函式,則是做了這樣的修改

CSceneTraveler::TravelResult CNode::VisitBy(CSceneTraveler* rpTraveler)
{
    if (!rpTraveler) return CSceneTraveler::Travel_InterruptAll;
    CSceneTraveler::TravelResult res=rpTraveler->TravelTo(this);
    if (res!=CSceneTraveler::Travel_SubTree) return res;  // don't go sub-tree
    if (m_ChildList.size()==0) return CSceneTraveler::Travel_Interrupt;
    ChildList::iterator iter=m_ChildList.begin();
    while (iter!=m_ChildList.end())
    {
        res=(*iter)->VisitBy(rpTraveler);
        if (res=CSceneTraveler::Travel_InterruptAll) return res;
        ++iter;
    }
    return res;
}

 

其實會這樣修改設計,還有一個原因。我的Scene Graph以及Culler是放在繪圖核心的模組裡,而Picker是在遊戲層的物件,為了保持物件之間關係的乾淨清爽,所以我並沒有直接用簡單的方法,把Picker綁在Scene Graph裡,而是將這個Traveler抽離了出來。

未來還會不會有其他的物件可以繼承這個相同的行為,我也不知道。

DirectX Shader 材質  

Posted by 藍斯洛 in

最近拿起了3ds max,玩一玩裡頭的DirectX Shader 材質。發現它其實還挺好用的。

首先先將材質換成DirectX Shader

max_1

然後材質的參數就會換成這樣的介面

max_2

第一個,DirectX Shader的欄位,可以讓我們指定使用的Fx檔案,第二個Parameters的欄位,裡面所有的參數,都是從Fx檔案裡定義的。只要照著標準的規則寫,參數便能夠列在這上面,讓製作人員去調整。同時在做模型輸出的時候,也同樣可以取得這些設定的值。

至於這些個參數是怎麼定義的,可以打開default.fx來看。


// light direction (view space)
float3 lightDir : Direction < 
    string UIName = "Light Direction";
    string Object = "TargetLight";
    > = {-0.577, -0.577, 0.577};
// material reflectivity
float4 k_a  <
    string UIName = "Ambient";
    > = float4( 0.47f, 0.47f, 0.47f, 1.0f );    // ambient
float4 k_d  <
    string UIName = "Diffuse";
    > = float4( 0.47f, 0.47f, 0.47f, 1.0f );    // diffuse
float4 k_s  <
    string UIName = "Specular";
    > = float4( 1.0f, 1.0f, 1.0f, 1.0f );    // specular
int n<
    string UIName = "Specular Power";
    string UIType = "IntSpinner";
    float UIMin = 0.0f;
    float UIMax = 50.0f;   
    >  = 15;

這個程式的語法,稱做 DirectX Standard Annotations and  Semantics,角括號裡面的字串以及其它變數的宣告,在Effect Fx中是沒有作用的,但是,在max中,就定義了Parameters的參數介面。

除了在DirectX SDK中有文件說明,max的網站上也可以下載到相關的文件(不過版本很舊了)。

用DirectX Shader材質有兩個好處,一是這個Effect Fx的檔案我們可以直接在遊戲程式中使用。第二是,這個材質能夠所見即所得的顯示出來材質效果。

所以,我很認真的在考慮,也許就直接把Effect Fx檔案拿來當做引擎中的材質使用好了。