2008年8月29日 星期五

Objects Streaming in Multi-Thread

這一篇,算是整理我自己的設計思路。

簡單來說,這功能是讀取資料流裡面的資料,然後產生相關的物件,包括遊戲中的物件,以及引擎核心的物件。當然免不了的,是必須放在多執行緒的環境中來進行,我們不能讓遊戲在進行中的時候,還要停下來等待讀取。

我做了個「不正式的」UML順序圖。

Object Streaming

幾個主要的步驟是這樣:在讀取之前,先做一些清除物件列表的事情,然後一個一個物件讀取,讀取完了,再對於這些讀取到的物件,連結它們的從屬關係。物件的產生是由物件的Factory函式來負責,Stream讀取了物件的Type Info之後,根據物件的類別呼叫對應的Factory函式。

物件的Factory函式要做幾個工作:產生新物件、將物件加入到Stream的物件列表中、讀取物件的內容資料。

在多執行緒的環境下,這樣的流程是沒有太大問題的,只要負責讀取的Stream不是各個執行緒共用的就好,而實際上也沒有必要這麼做。

但是遊戲中有幾種物件,資料量很大,很佔用記憶體空間,在設計上,我們必須讓這些資料變成共享的資料以節省不必要的記憶體浪費。而這些物件,在上面的流程裡,就會出問題了。

我用兩個不同的Stream,放在不同的執行緒裡,同時讀取一個共享的Object。由於物件的Factory函式中,並不會產生兩個獨立的Object,而是分享同一個Object,所以在讀取物件的內容資料時,就很容易發生資料打架的Race Condition。

為了解決這個問題,我又多開了一個執行緒。姑且叫做「共享資料專用執行緒」。這個執行緒的唯一工作,就是負責讀取這些共享物件的資料內容。執行緒會把工作存放在佇列中,一項一項的依序處理,這樣一來,資料打架的問題就不會再發生了。

當然,除了共享資料的物件之外,其他的物件也可以把讀取工作交辦給這個執行緒來做,不過我並不打算這麼做,因為這些物件並沒有資料打架的問題,沒有必要多花費一道手續跟時間,而且對這個專用執行緒來說,這麼多物件都交給它讀取,太操了...

2008年8月27日 星期三

Visual Studio中的GetLastError()

沒事,寫個書上看來的小Trick。

Windows API有一個 GetLastError()函式,可以取得前一次呼叫API的Error Code,我們用 Visual Studio跑程式的時候,可以在”Watch” 視窗中,輸入 ‘$err,hr’ (錢號err逗號hr),就可以得到當時的error code...

很方便...而且顯示出來的訊息還是中文的...

watch_last_error

2008年8月20日 星期三

設計模式之 Prototype 模式

以下關於Prototype模式的內容,是屬於我的理解,如果你們有不同的想法,歡迎來討論。

Prototype

上圖是設計模式的書裡頭,Prototype的架構圖。

但是我覺得這是一個容易讓人誤解的架構圖。Prototype的精神不在於架構,而在於方法,也就是那個 Clone 函式。

舉個例子,網路遊戲玩過吧,裡面有很多怪物讓你打死來練功。程式裡面,怪物是怎麼產生的?

Monster* monster=new Monster();
monster->setName("Kill Me");
monster->setHP(100);
monster->setType("Animal");
monster->setPosition(100,100);
monster->setSkill("Flee");

這樣,我們有了一隻HP 100,動物形態,名字叫做"Kill Me",會逃跑的怪物。

那麼,如果在這個場景裡面,要30隻怎麼辦? 用個迴圈,把上面的程式碼重複跑30次。但是這樣做,有點笨又沒效率。

另外一種方法就是,複製這隻怪物。在怪物類別裡增加一個Clone函式,把自己的所有屬性,包括名字,HP,形態,技能等等,都設定給複製出來的新怪物。這樣的動作,都在類別內完成,效率會比較好些,同時對於外部的應用程式來說,只需要呼叫一個 Clone 函式,哪一天如果怪物又多了個新的屬性的時候,外部應用程式是不需要修改的,只需要在怪物的 Clone 函式裡,再加上複製新屬性的程式碼就好。

Monster* Monster::Clone()
{
Monster* clony=new Monster();
clony->m_Name = m_Name;
clony->m_HP = m_HP;
clony->m_Type = m_Type;
clony->m_Position = m_Position;
clony->m_Skill = m_Skill;
return clony;
}

這隻原始的,用來複製的怪物,就是一個 Prototype。

至於最上面的那張圖,我的想法是,我們可以做一個叫做Prototype的抽象類別,裡面只有一個函式 Clone ,需要做複製的類別,可以藉由繼承 Prototype 抽象類別取得共同的介面,然後,當然,還是要實做自己的 Clone 函式。 Prototype 類別可以做為判斷可否複製之用。ConcretePrototype1, ConcretePrototype2 可以是怪物,可以是花草樹木,可以是車子,可以是八竿子打不著的物件類別,它們之所以繼承 Prototype 類別,純粹只是告訴應用程式說 -- 我這個物件是可以複製的。

2008年8月15日 星期五

這對我們來說,是壞事也是好事

卸任的阿扁繼續出包。

這幾天,抖出來阿扁他們家在瑞士有好幾億台幣的外匯帳戶。這次阿扁很快,馬上就出來道歉了,說是選舉結餘款。然後又依照慣例斷尾求生,說不關他的事,是家裡人弄的,他後來才知道。不過,這次的尾巴是吳淑珍。

另外一個阿扁的慣例咧,就是再往另外一個政黨身上扯。這次也沒少就是了。

我對這些說法是很懷疑的,邏輯上怎麼講也講不通。如果是光明正大的公共用途,這麼愛台灣的阿扁與阿珍,為什麼要把錢存到國外去? 又為什麼戶頭要開在別人的名下?

不過,這件事對我們來說,是壞事也是好事。

壞事不多說了,一個前任總統被外國扯出洗錢疑雲,已經夠丟人現眼了。好事咧,至少以後沒哪個做官的敢到瑞士洗錢了吧?

hmm......搞不好以後用更高明的手法.......

2008年8月13日 星期三

你就是最佳的共犯

這次寫的東西是18禁。

前幾天租了一部片子--『Live殺人網站』。我並不愛看恐怖片,只是這部片子的故事很有創意。劇情是,有個極聰明的兇手,設了一個網站, 實況轉播對被害人的虐殺過程,而且,越多人點閱觀賞,被害人就死得越快。

虐殺的手法要講嗎? 喔,好吧。

第一個手法是,將被害人砍幾刀讓他流血,然後注射『抗凝血劑』(這個,我前一陣子才在CSI裡面學到過...),隨著點閱人數越來越多,抗凝血劑就注射越多,被害人因為血液沒法凝固止血,最後變成大量失血而死。

第二個手法,是用很多很多盞高熱量的照明燈,將被害人『曬』死。第三個被害人是FBI的探員,被兇手抓來泡在水缸裡慢慢的加硫酸,越多人看轉播,硫酸加越多...

犯案要有動機。兇手因為自己的父親自殺時,被人拍下了影片在網路上到處流傳,才會想,好,既然你們這麼愛看,就讓你們看個夠!!

虐殺過程是恐怖還加點噁心的,不過最恐怖的還是這個殺人的創意設定。一個被虐殺的影片,實況轉播,如果是你,你會去點閱嗎?

我們在網路上,什麼沒有,閒來無事的朋友最多,心血來潮就流傳一下,一時好奇就去點閱看看,說真的,我一定會收到這樣的轉發,點閱嘛....我看是忍不住,好奇心百分之百會獲勝.....

然後就成了幾十萬個殺人共犯之一.....

想到就心裡發毛...

2008年8月12日 星期二

繪圖執行緒的同步化

嗯...果然不出所料,一開始實做繪圖執行緒,就面臨了幾個問題。

第一個問題是資料的Race Condition,之前已經預期到會有這類問題發生,不過沒有防範完全,所以在一些共享物件的『參考計數』上出狀況。用了兩個函式 InterlockedIncrement 以及 InterlockedDecrement 改掉了這問題。

另一個問題就是當初沒有想到的,也算是對多緒的程式設計還瞭解的不夠透徹所致。把前面的這張圖拿回來看。render_thread一開始,我採用的設計是右邊的這個 Consumer thread 的架構,紅色的邏輯執行緒送指令到佇列,綠色的繪圖執行緒負責從佇列中取出指令來執行。

做法看起來還不錯,但是實際執行就有問題。

問題出在,邏輯執行緒是遊戲程式的主執行緒,而主執行緒會分到相對比較多的執行時間,於是,邏輯執行緒所送給佇列的指令,繪圖執行緒在分到的時間不夠多的狀況下,就沒有辦法及時的處理完。結果就是,邏輯執行緒的Frame rate很高,但是實際畫面的更新率卻非常非常低。

我試了幾個方法,將繪圖執行緒的優先權提高、讓邏輯執行緒Sleep()一下....sync_render_thread最後使用了底下這張圖的方法,在邏輯執行緒與繪圖執行緒之間,建立一個同步的機制,讓邏輯執行緒在接到同步信號之後,才進行下一個Frame的運算。

把繪圖執行緒的優先權提高似乎沒什麼用處,執行緒分到的時間還是少得可憐。而用Sleep函式讓邏輯執行緒睡一下的方法,可以讓佇列中不要累積太多的指令,不過同步的效果並不好,兩個執行緒時快時慢,反而不好控制。

比較起來,同步信號的方式算是比較好的方法。

 

PS. 文章中的兩張圖片,取自Intel在GDC 2008發表的演講內容。

2008年8月6日 星期三

程式碼的怪味道

第一次看到這個概念,是在講敏捷軟體開發 ( Agile Software Development ) 的這本書(【敏捷軟體開發--原則、樣式及實務】,碁峯出版)中,最近在有關重構( Refactoring ) 的兩本書裡,又看到這些怪味道的描述。想起當時看到這些描述時,我的心裏可真是『點頭如搗蒜』,百分之五百的認同啊!

過去我們所寫的程式碼,忽略了分析與設計的階段,今天說,我們來做個遊戲吧,明天就開始Coding,然後遊戲程式的主架構就建立起來了,然後加上主角人物,加上地圖,加上場景,加上遊戲介面,加上各式各樣的功能與物件......

然後...程式碼就改不動了。

主角人物跟場景的關聯性太高,所以我們要加一點程式碼讓他飛行的時候,場景的程式碼要跟著修改,地圖的程式碼,遊戲介面的程式碼,也都要做修改,『牽一髮而動全身』。

這樣也還好。至少我們還知道要從哪裡開始去改。怕就怕在,程式碼的流程跟架構都難以理解的時候,我們為了修改出所要的功能,只能『挖東牆補西牆』,可沒想到,程式碼這裡少一塊,那裡少一堆。我們花了好大功夫,修修補補之後,功能是完成了,但是程式碼卻更加改不動了。

還有,不知怎麼搞的,就在一剎那間,Bug開始像瘟疫一樣擴散。好不容易在這裡修好了一個Bug,卻在別的地方乒乒乓乓冒出五六個新的問題,於是乎,Bug是越改越多,我們對自己的程式碼也越來越沒自信。

所以我們常常會說,『拆掉重做還比較快』。

但真的是這樣嗎?

如果這個程式碼可以很容易拆掉的話,我們在修改的過程中也就不會有那麼多問題了。同樣,重新做一個也未必會比較好,除非我們回頭把分析跟設計做好,否則這些問題還是一樣會發生。

治療這些問題,只能治本,不能治標。從一開始,我們就要把需求、分析、設計這三項工作做好。當然,需求絕對是會變的,尤其是遊戲專案這樣的程式。一旦需求變更,我們就必須針對這些修改的部分,再做一些分析設計,調整我們程式結構裡面不適宜的部分,隨時隨地保持整個程式碼的架構清楚,容易理解。也許這樣一來,原本三五分鐘可以做完的功能,要花掉一天的時間,但是我們所得到的,是一個一直都像重新做過一樣的程式碼,而不是一個一步步變得僵化、脆弱、難以理解、甚至Bug叢生的程式碼,誰說這不值得?

PS. 書裏面提出的怪味道,包括:
1. 僵化性 (Rigidity)
2. 脆弱性 (Fragility)
3. 固定性 (Immobility)
4. 黏滯性 (Viscosity)
5. 不必要的複雜度 (Needless Complexity)
6. 不必要的重複 (Needless Repetition)
7. 晦澀性 (Opacity)

2008年8月3日 星期日

Graphic Debugger ( Part VI )

pix_user_event1

這是PIX中,抓取單一Frame資料所得到的Events。一個Frame中的D3D API呼叫非常多,我只是用一個D3D的範例程式,就已經上千個事件了,如果是遊戲程式的話,用上萬個事件起跳來算,應該不過份。

那麼問題就在於,我們怎麼去找到想要看的事件?

Direct3D提供一組API,可以在PIX中對抓取的資料做一些記號。例如說,我們可以利用D3DPERF_BeginEvent, D3DPERF_EndEvent這兩個API函式來設定User Event。

我們在程式碼裡頭先做這樣的修改,

D3DPERF_BeginEvent(0xffffff00,L"Begin Render Text & UI");
RenderText();
g_HUD.OnRender( fElapsedTime );
g_SampleUI.OnRender( fElapsedTime );
D3DPERF_EndEvent();

在RenderText()之前,先呼叫Begine Event,UI Render之後,呼叫End Event。這樣一來,我們在抓取單一Frame的資料的時候,就會得到User Event。

pix_user_event_2

User Event後面的字串,是我們在Begin Event中所給予的參數字串。Events圖表的上方有一個Find Event的按鈕,我們可以利用來找到我們自己定義的Event。

2008年8月2日 星期六

繪圖執行緒的設計

把繪圖功能分離成一個獨立的Rendering Thread一直是我想要做的一種設計,將遊戲程式的執行緒與繪圖的執行緒分開,理論上在多核心的電腦環境中,可以提高不少的運算效能。但在這裡面,還有個問題 -- Race Condition。

這是個很大的困擾。我要怎麼樣避免遊戲邏輯與繪圖功能存取到相同的繪圖資源物件?舉例來說,繪圖功能在讀取Vertex Buffer內容進行繪圖的時候,如何避免遊戲邏輯將資料寫入同一個Vertex Buffer?

不能將Vertex Buffer這些繪圖資源鎖在執行緒裡,繪圖物件很多,存取頻率又很高,這麼頻繁的上鎖解鎖動作,只會耗掉不必要的效能,沒有多少好處。想了很多,卻一直沒有好的方法。


一直到今年的Game Developer Conference,我看到了這張圖。

兩種模式差異並不大。紅色是遊戲邏輯的執行緒,綠色是繪圖執行緒,中間的藍色代表的是繪圖指令。邏輯執行緒將繪圖指令送到佇列中,繪圖執行緒從佇列中取出指令執行。

一個很簡單的架構,就將邏輯執行緒與繪圖執行緒切了開來。

繪圖執行緒擁有繪圖資源物件的存取權,邏輯執行緒不能直接存取繪圖資源物件,只能透過繪圖指令。這麼一來,就沒有兩個執行緒存取相同資源物件的問題,也就能夠避免Race Condition的發生。

當然了,這只是架構設計,實作層面也應該還有很多目前還沒看到與想到的問題,現在正準備寫個程式來實現這樣的架構,看看到底會面臨什麼樣的問題。

等實做程式出來以後,再來做個記錄吧...

PS. 文章中的圖片,取自Intel在GDC 2008發表的演講內容

Update 8/3 : 在Nebula Device這個引擎上,看到Rendering Thread是採用Proxy Pattern的方式來做,邏輯執行緒中用到的物件都只是Proxy物件,真正的繪圖工作是Proxy所指向的物件來實際執行。這方式也不錯,不過我還是 比較喜歡Command Queue的方式。