2009年12月27日 星期日

3dsMax Exporter Plug-in (2)

在實際執行之前,要先改一些程式碼。

打開專案程式的主檔案MyExporter1.cpp。

clip_image002

這個就是我們的Exporter主物件。從SceneExport繼承而來。

DoExport函式是實際進行輸出工作的入口函式。其他還有一些設定的函式,不過只有設定輸出檔副檔名的Ext函式比較重要。

int MyExporter1::ExtCount()
{
    #pragma message(TODO("Returns the number of file name extensions supported by the plug-in."))
    return 1;
}

const TCHAR *MyExporter1::Ext(int n)
{       
    #pragma message(TODO("Return the 'i-th' file name extension (i.e. \"3DS\")."))
    return _T("me1");
}

const TCHAR *MyExporter1::LongDesc()
{
    #pragma message(TODO("Return long ASCII description (i.e. \"Targa 2.0 Image File\")"))
    return _T("My Exporter Sample 1");
}
const TCHAR *MyExporter1::ShortDesc()
{           
    #pragma message(TODO("Return short ASCII description (i.e. \"Targa\")"))
    return _T("My Exporter 1");
}

我們把 ExtCount, Ext, LongDesc, ShortDesc 函式簡單實做之後,重新編譯一次。

然後,從 Visual Studio Debug 執行它。

跑不起來是正常的。

打開專案的 Property 設定,在 Debugging –> Command 那一項,把 3ds max 的執行檔設定進去。

clip_image002[9]

然後我們可以 Start Debugging。

Visual Studio 會啟動 3ds max,而 max 會將我們的 plug-in load 進去。

我們的 exporter 是在 export 檔案時才會用到的,所以,先 export 看看。

我們會在存檔類型那裏,找到我們的輸出檔格式。

clip_image001

到這裏,我們的 exporter 算是可以掛上 max 系統裡了。

2009年12月24日 星期四

3dsMax Exporter Plug-in (1)

序言

Exporter 是我們在製作 3D 遊戲時,非常常用的一項工具程式。由於美術所製作的 3dsMax 模型,對於 3D 引擎來說,資料過多且複雜,所以我們需要借助 Exporter 將模型資料轉出成我們所需要的資料格式。DirectX X-File 即為其中一種格式。

3dsMax 利用了抽象介面的概念,定義了非常多的 Interface,Plug-in 設計者可以繼承這些 Interface 來實做,3dsMax 便可以透過抽象介面虛擬函式的動態連結,在需要的時候呼叫 Plug-in 所提供的實作物件。

Step by Step

第一步,當然是先把 3dsMax 安裝起來,同時,要安裝 3dsMax SDK,是的,Max 也有 SDK。

接著,為了自己的便利,最好把 "3ds max Plugin Wizard" 安裝起來。通常 SDK 的目錄文件裡都有,如果沒有,就花點時間請 Google 大神幫忙。

安裝完畢,Visual Studio的專案裡,會多了一種專案。

image

建立一個新專案 ( MyExporter1 ),當然,要選取3ds max Plugin Wizard。

然後Wizard會出現以下的選擇畫面。

clip_image001

看到麼? 沒在騙人,真的有很多種類型的 Plug-in。

我們這次需要的是 File Exporter 。

選擇以後,後面兩個步驟就照著需要,填寫裡面內容。

然後,Wizard 就會協助建立一個 Plug-in 專案。

有了專案,編譯看看。

如果,我們前面的設定沒有問題,編譯通常就沒問題。我們會得到一個附檔名是 dle 的檔案,直接輸出在 3dsMax 的 plugin 目錄下。

dle 檔案實際上就是一個 dll ,改成 dle 只是表示這是一個 exporter 。

2009年11月7日 星期六

Unreal Development Kit

Unreal把它的開發工具,很完整的開放出來了。免費授權。如果你自己做了個遊戲拿出來賣的話,只要收入不超過5000美金,是不用抽稅的。

據說Unreal在大陸的授權金已經是低到很離譜了,完全是為了打開市場考量,所以現在把這個開發工具免費授權也就真的沒什麼好驚訝的了。

下載網址在這裡:

Unreal Development Kit

昨天很快的用我的「光世代」下載了。

不過為什麼會變成簡體中文的咧?

udk

 

而且說真的,打開程式以後完全不知道要怎麼開始編輯一個關卡或場景,有沒有什麼 Quick Start  或是 Tutorial 之類的文件可以看看啊?

2009年11月6日 星期五

T-shirt for twins

網路上晃啊晃的,看到這樣兩張圖片,常碰電腦的應該會會心一笑吧...

500x_tumblr_ks3cysr6YE1qzpwi0o1_500

500x_copy-paste-twins_01

2009年10月13日 星期二

家族日本行 (3)

溫泉旅館度過了一晚之後,隔天回頭往東京。

因為是搭乘遊覽車在東京市區裡,所以,還真的沒辦法參觀很多地方。不過說實在的,東京我也逛過好幾次了,自己坐地鐵還比較能夠跑更多地方。

DSC00706

這個大燈籠真的沒什麼好拍的,不過還是得拿來當做『到此一遊』的記號。

接著被遊覽車帶去參觀了一個專賣高檔精品的百貨公司,這裡倒是有點不一樣的東西。

DSC00711

這個是百貨公司裡頭的樓層場館導覽,整個都是觸控螢幕。不知道台灣的百貨公司有沒有這種東西。

 DSC00712 DSC00713DSC00714

百貨公司館外草坪上剛好有人在吹熱氣球

跟著呢,遊覽車載著我們到台場的飯店住房,然後就自由活動逛街。

台場逛街的戰利品,就是在娃娃機用六個硬幣夾到的三個娃娃。雖然忘了拍照存證,但是,日本的夾娃娃機真的很佛心啊.... 台灣,尤其是台北的夾娃娃機,根本就是吃角子老虎....

晚餐自行解決,所以.....一定要築地壽司!!

DSC00734 DSC00730 DSC00732

本來是想坐在櫃台的,就像日劇裡演的那樣,一邊點壽司一邊跟廚師聊天。但是....還是算了吧....

現在想到,我們在隔天早上出發到機場之前,應該拍張全家福合照的....

就拿在溫泉旅館拍的來做結尾吧....

DSC00697

2009年10月8日 星期四

家族日本行 (2)

隔天,坐上遊覽車,往箱根前進,沿路走了幾個景點。

第一個來到的是小田原城。

日本戰國時代,北条氏發跡的小田原城。

旅行之前,剛好在玩「信長之野望」,所以對這個城特別有興趣....XD

北条氏很猛,從小田原城發跡,曾經擋住了武田信玄的攻擊,一直傳了五代,每一代的領土都有擴張,直到豐臣秀吉用好幾路大軍包圍,北条氏才被吃掉。

不過我在玩的「信長之野望」裡頭,北条氏老是活不長,歷史模擬遊戲終究還是沒辦法模擬歷史。

DSC00644

DSC00648

天守閣的外觀

DSC00645

天守閣裡面有戰國時代的歷史文物展覽。不能拍照的,拍了一張就被制止了,所以這張照片很稀有....

接著咧,坐小火車上山。這個小火車跟阿里山的一樣,也是用之字形的方式上山。

DSC00654

車站裡頭

DSC00656

乘客可以直接看到駕駛員在做什麼。深深覺得台灣的火車跟捷運也要這樣透明才行。

DSC00667

上了山,參觀一個雕刻公園。

DSC00684

然後坐這個所謂的「海賊船」到湖的對岸。其實我不喜歡這種船,在日本嘛,就應該讓我們坐坐以前日本人當海盜的那種船啊!

DSC00691

跟著來到了一個叫做「忍野八海」的地方。八海指的是八個小池塘。這地方就有點歷史的感覺了。雖然裡面還是在賣現代化的土產。

(從這時候開始,遇到大陸旅行團了,所以開始用台語交談....)

接下來,就到了溫泉旅館休息泡湯。

hmm....全裸的泡湯就不能提供照片了....

因為不能帶相機進去....

2009年10月6日 星期二

家族日本行 (1)

九月底,咱們一家大小 (七個大人外加一個兩歲半的小朋友),到日本玩了五天。實際上是三天啦,迪士尼樂園玩一天,坐車到箱根的溫泉,沿路上參觀小田原城跟其他景點,在溫泉旅館住一晚,然後回頭到東京的景點晃一晃。

DSC00595

桃園機場大廳

DSC00599

兩歲半的小朋友和她媽媽(也就是我老妹)

小朋友超可愛,殺掉相機裡一半以上的記憶體......

DSC00608

迪士尼專車

DSC00617

迪士尼樂園拍不了全貌,只能拍大門的一角。

據說迪士尼樂園本來是要建在台灣的,不知道為什麼最後被東京給搶了去。總之脫不了短視近利外加豬腦袋這兩個原因。

迪士尼玩了一天,可是沒什麼照片。晚上的遊行表演倒是有影片。

呵....

對,影片拍得很不好....

2009年8月24日 星期一

Dirty Coding Tricks

Dirty Coding Tricks by Brandon Sheffield

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

今天在Gamasutra上看到的一篇文章。

遊戲開發有時程壓力,而當我們發現一個很難解的Bug,時間又已經來不及的時候,我們該怎麼辦? 這篇文章就舉出了幾個偷吃步的故事。

頭一個故事就很慘了。

在一個PS2遊戲即將要發行的時候,測試團隊發現,在第一個關卡裡,玩家只是靜靜的站著就偶而會造成crash,而且更不幸的是,是在燒成光碟之後,這個問題才會出現。

於是開發團隊為了解決問題,不斷的修改程式,燒成光碟,然後測試,弄了十幾個小時( 為了等程式 crash,只好拿 World of Warcraft 打發時間.... )。

後來奇蹟出現了。有人發現,一開始攝影機向右轉個90度的話,程式就不會出問題,於是乎,PS2的版本,就在攝影機轉個角度之後,順利通過測試而發片。而已經上架的另外兩個平台的版本,攝影機角度還是維持原先的設計。

我的結論是:寫程式,也需要奇蹟。

第二個故事,真的就是偷吃步了。

遊戲程式中所用到的資源,例如一大堆的檔案,在檔案系統中通常會想些編碼的方式當作索引來存取這些檔案。這個故事裡的團隊很倒楣,他們將檔案的檔名CRC32編碼,同時也將檔案內容做CRC32編碼,用這兩個值組成64位元的索引來存取檔案內容。

開發了兩年,處理了幾萬個檔案,從來沒有發生索引值衝突到的狀況。

可是,它真的發生了。兩個不同內容的檔案,卻有完全相同的檔名(也不知道為什麼要這樣設計....),而且檔案內容在編碼之後也得到相同的CRC32編碼--這下子事情大條。

改檔案系統,改索引方式,都是個大工程,而且誰知道改了之後會不會發生同樣的狀況? 這時候,再怎麼完美的編碼方式,都沒辦法給人信心了。

沒時間了,只好用一個最簡單的偷吃步解決這個問題 -- 把其中一個文字檔案打開,在最後面加一個空格,然後存檔。

於是乎,遊戲如期發行。

 

當然啦,這些方法都不是好的解決方式,而且會帶來後續的問題,不管是程式穩定度還是程式維護上的問題。

不過,對我們老是在有限時間跟疑難雜症打仗的人來說,這些故事是會讓人會心一笑的。

2009年8月18日 星期二

Design Patterns for Game Programming -- Reference Count

這是提供入行的新人教育訓練用,我們自己整理的Game Programming中的Design Patterns。我們所想到的第一個項目,就是Reference Count。所以就先將它整理出來了。

既然是寫設計模式,自然必須依照設計模式經典的編排方式來做。

以下進入正題。

 

Design Patterns for Game Programming -- Reference Count

使用時機

遊戲程式中充滿了使用記憶體資源的物件,其中有些物件的特性是,佔用大量的記憶體,又常常重複出現在遊戲中。例如,3D遊戲中的模型、貼圖,2D遊戲中的圖片資源等等。

clip_image002

(場景中重複出現的樹木就包括了重複使用的模型與貼圖) ( Screen-Shot from World of Warcraft )

針對這種物件,我們會將它設計成共享的資源物件,以節省記憶體的使用量。同時,我們也在共享資源物件中,加入參照計數(Reference Count),以便記錄每個共享資源所被參照的數量。

目的

將共用的資源做有效的管理。改善資源與記憶體的使用。

設計實作

clip_image003

架構上主要有三種物件。

Shared Resource :

也就是共享的資源物件,像是貼圖、模型、圖片等。成員變數m_iReferenceCount記錄的就是參照計數,而IncRefCount與DecRefCount則是用來對Reference Count遞增與遞減之用。每個Resource有一個唯一的Key值(例如,資源檔案名稱或是某種編碼後的整數)。

Resource Table :

存放Shared Resource的地方,通常會使用Hash Map、Binary Tree或是其他能夠快速的從Resource Key值搜尋物件的資料結構。

Resource Manager :

做為與應用程式溝通的介面物件。提供兩個最主要的成員函式 : Query Resource 與 Release Resource。

clip_image005

應用程式透過呼叫Resource Manager的Query Resource函式,來取得共享資源。

首先會在Resource Table中搜尋是否有對應Key值的資源存在,如果存在,就將資源的參照計數遞增之後,傳回給應用程式。如果不存在,就產生一個新的資源( 此時新資源的參照計數為1 ),插入Resource Table中,然後傳回給應用程式。

clip_image007

另外,應用程式也透過Resource Manager的Release Resource函式來釋放資源。

首先Manager會先將Resource的參照計數遞減,並且取得結果。如果參照計數小於或等於0,表示已經不再需要這個共享資源,就先從Resource Table中將資源移除,然後,刪除這個資源。

多緒環境下的設計實作

有兩項必須做到Thread-Safe,一是Resource Table的存取,避免多個執行緒同時修改Table內容以及相關的索引資料,另一個則是Reference Count的遞增與遞減計算,避免多個執行緒同時修改,造成資料錯誤。

使用設計上的限制

1. 在這個機制下,共享資源的生成與銷毀都必須是透過Manager的Query / Release來負責,應用程式要避免直接使用new / delete來生成 / 銷毀共享資源。一個可用的設計方式是,將資源物件的建構子與解構子封裝起來不公開,封鎖應用程式對共享資源的new / delete權限,同時宣告Manager為friend class,把權限讓給Manager。

2. 共享資源的Inc / Dec Reference Count操作必須是成對的,否則容易出現Memory Leak ( 當 Inc Reference Count次數大於 Dec Reference Count時 ) 或是使用到無效的指標 ( 當 Inc Reference Count次數小於 Dec Reference Count時 )。這項限制,可以透過Smart Pointer的設計來改善。

2009年7月31日 星期五

Code Review

前幾天在想有關Code Review的事情的時候,網路上瀏覽到這麼一張圖。

wtfm

哇哈,一張圖片,勝過千言萬語啊......

2009年7月10日 星期五

遊戲程式的設計模式

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

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

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

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

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

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

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

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

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

反正都不是好結果。

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

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

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

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

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

2009年6月19日 星期五

Material Script and Technique

我想,大概沒有什麼繪圖或遊戲引擎不在引擎裡頭使用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 都加進去,也是不小的工程。

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

2009年5月18日 星期一

BattleStar Galactica看完了

  image

image

(圖片來自BattleStar官網)

終於,把BattleStar給看完了。

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

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

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

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

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

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

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

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

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

哈。

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

2009年5月6日 星期三

執行緒函式的設計

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

一開始是一個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;
}

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

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

2009年4月24日 星期五

What Went Wrong? Learning From Past Postmortems

這是今天在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.

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

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

全部。

一個都不少。

2009年4月22日 星期三

Scene Graph Traveler

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

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抽離了出來。

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

2009年4月5日 星期日

DirectX Shader 材質

最近拿起了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檔案拿來當做引擎中的材質使用好了。

2009年3月19日 星期四

Intelligent Mistakes

Intelligent Mistakes: How to Incorporate Stupidity Into Your AI Code

by Mick West

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

每天看Gamasutra,偶爾會看到一些好東西。

這個主題叫做「有智慧的錯誤」。

人都是會出錯的,所謂「人有失手,馬有亂蹄」,但是電腦AI不會出錯,只有計算夠不夠完整的問題。

但是這樣的遊戲,玩起來就死板板的,也喪失了一些趣味。

作者舉了他自己做的撞球遊戲當做例子,撞球遊戲的AI不難,就是簡單物理碰撞計算而已,對電腦來說,出桿可以非常非常的精準,但是「人」就不行了,要不就打歪,要不就力道不對,總之是不可能精準的像電腦一樣。

所以,這樣的遊戲玩起來,就是跟一個精準得要命的電腦對手在玩。不但每顆球都打得很「千」,連不小心放的「嗆斯」都沒有。

這就很無趣了。

AI is “too good”.

所以,要做一些精心設計的錯誤,讓玩家覺得他佔到便宜,好比說,偶爾故意放個「嗆斯」、偶爾「突槌」一下、偶爾不小心把母球打進洞....

這樣就有趣多了,人生本來就是充滿了不確定啊....

2009年3月12日 星期四

Remote Debug

遇過這樣的狀況嗎?

『我們的程式,在自己的電腦上執行,一切正常,但是交到別人手上執行,卻常常出問題,更不要說是交給老闆測試了,老闆的電腦特別奇怪,好好的程式交給老闆就一定會出事。』

這種時候,就是放61點天賦大絕的時候了。

這個大絕叫做『遠端除錯』,Remote Debug。

概念是這樣,我們在近端--也就是我們自己的電腦--用VC或是其他的編譯工具進行除錯,程式則是在遠端電腦(也就是老闆的電腦)上執行,Trace/Debug的做法,與我們在自己的電腦上除錯是相同的,所不同的是,我們控制的是老闆電腦上的程式。

用這種方式除錯,遠端電腦上是不需要安裝任何的編譯工具的,只需要兩種東西。

一個是程式的執行檔和相關的資料檔案,另外一個就是稱做Remote Debug Server或是Remote Debug Monitor的程式。這個Remote Debug Monitor是一個很小的程式,通常放在VC或是編譯工具的安裝光碟中,請拿出來安裝在遠端電腦上。

然後,在遠端電腦先將remote debug server跑起來,等待近端的連線。

接著我們在近端電腦打開程式專案,做一些設定--每一種編譯工具的設定方式都不同,甚至不同版本的VC++都有不同的設置方式--之後,開始debug run。這時候,遠端電腦上的執行檔就會被跑起來,而所有的debug output訊息,會顯示在近端的編譯工具裡。同時,也可以設定break point,去trace 遠端電腦上的程式流程。

這樣,我們就能夠很快的發現bug,解決bug,也就不會在老闆面前滅團了。

2009年2月6日 星期五

邪惡的巨集

巨集(Macro)是邪惡的。

它很好用,可以簡化我們的程式碼,節省很多Coding的時間。

但是它暗地裡在破壞我們程式碼的物件架構,在物件封裝中到處鑽孔。

所以,前兩天,我將程式碼裡頭的

#include "windowsx.h"

給拔掉。

這個標頭檔,定義了很多巨集,有些是簡化windows訊息程式碼,有些是簡化windows API。平常是很好用的啦,但是,好死不死的,我有個樹狀結構的物件,裡頭有幾個函式-- GetFirstChild, GetNextChild, ... 就跟windows API的巨集衝突到了。

好端端擺在物件中的GetFirstChild函式,被windows API的GetFirstChild巨集取代掉,所以編譯器瞬間開始混亂,看不懂我的程式碼在幹什麼,連錯誤回報也是亂報一通不知所云。

花了時間除錯,拔掉了這些個讓人偷懶的巨集。

所以這巨集真不是好東西。

裹著糖衣的毒藥。

Code Complete的作者在書裡也有說,巨集,能不用就不用,務必謹慎使用。

是啊,別偷懶了,該拔掉的就拔掉吧。

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

其實今天一早,還遇見了另一個類似的問題。

我在數學函式庫裡,定義了一個靜態常數PI (就是圓周率啦),還特別放在一個Math物件中,用Math::PI這樣的語法來存取,避免這種常數定義衝突的狀況。

class Math
{
...
static const float PI;
};

不過,又來了,好死不死,Max SDK裡面也定義了一個PI,而且是直接#define,

#define PI  ((float)3.1415926535)

結果我怎麼辦? 我只能先引入自己數學函式庫的標頭檔,然後再引入Max SDK的標頭檔,順序絕對不能換,否則編譯器就會開始錯亂。

2009年1月23日 星期五

又多了個小外甥

今天才寫,有點晚了。

一月十五日,老妹生了第二胎,是個3240克的健康胖小子。

本來,還想著要他跟姐姐一樣過完年才出來,不過這小子的確跟姐姐一樣,也是大約37週就把媽媽的營養吃完,準備出來。

也好,兩人的生日都是正月十五,姐姐是農曆,弟弟是國曆。

看起來黑黑的,似乎又是個小黑皮。

這個胖小子,也是妹妹的夫家期盼了很久的男丁,上頭已經有三個姐姐了。

昨天老妹家裡在幫小娃娃取名字,剛好在MSN上撞見,我就跟著參考了意見。

「宗」: 不要,太菜市場了。

「宸」: 網路上查了字典,這個字好。辰是龍,屋頂下的龍,表示這個字是帝王的住所,也可以引申為帝王的意思。

「菘」: 看起來不錯,仔細一查,哇咧,原來這個植物就是「白菜」。算了,還是不要這個字吧。

「瑋」、「瑜」、「瑄」: 都不錯,都是「美玉」。不過就是有點像女生的名字,轉念一想,周瑜也用這個名字啊,還好啦。

最後選了「宸瑋」兩個字。很不錯吧,帝王的美玉。

小娃娃才出生一個星期,會不會對他期望太高了? 哈....

老妹照了幾張照片,不過都有點糊,還是小小張的看就好,放大看會更糊。

照片 001

照片 002

2009年1月13日 星期二

Finding Oriented Bounding Box

前一陣子發現計算OBB的演算法,研究了幾天,本以為大致都了解了,但深入去思考推導之後,卻才發現,原來我是錯得離譜,Totally Wrong!! (翻譯叫做 "一整個錯!!")

於是又從頭開始,一步一步從數學的角度來看。

觀念還是一樣,把美術模型的每一個頂點,都當做是取樣點,然後用Least Square Fit的方法,找出OBB需要的中心點以及三個軸向。

OBB的中心點取得,根據的是偏微分取極值的方法,三個軸向呢,則是用Principal Axis的概念,從Eigen vector來取得。

底下是兩個參考資料。

http://mathworld.wolfram.com/LeastSquaresFitting.html

http://www.geometrictools.com/Documentation/LeastSquaresFitting.pdf

這個推導,背後的數學有點小複雜,不過,大概都是些矩陣向量的運算概念。

最後呢,得到一個取得OBB的步驟。

1. 先建立一個可以解出Eigen value, Eigen vector的函式庫(或者去抄一個)

2. 把所有頂點座標加起來,找出平均值,就是OBB的中心。

3. 對於每一個頂點,計算與中心點在 x,y,z 三個方向的誤差值 X,Y,Z ,取得兩兩相乘的值,XX, XY, XZ, YY, YZ, ZZ,把每個頂點所算出來的這六個值,全部加起來平均。然後塞到一個3x3的矩陣內。

4. 把這個矩陣丟給Eigen System求解,拿回三個Eigen vector。

5. 用這三個Eigen vector得到的座標系統,轉換原來的頂點,找出在這個座標系統中的Box長寬高。

2009年1月6日 星期二

數學才是王道

話說,元旦連續假日,大家都放假了,我還要上班。

把Code拿出來看看,想說處理一下引擎裡Bounding Box的計算。

找了一些Open Source的參考Code來看,想看看有沒有什麼比較有效率的演算方式來計算Bounding Box。

然後,我就愣住了。

眼前的這個演算法,把所有頂點的資料拿去算一個covariance matrix,再用這個matrix求解eigen vector以及eigen value,然後....就算出了包圍所有頂點的最佳的Oriented Bounding Box。eigen vector是OBB的三個軸向。用找到的三個軸向,來計算OBB的軸半徑。

我傻眼了。

eigen vector有那麼神嗎? 連這個都能算。還有covariance matrix究竟是什麼東西啊?

開始研究了幾天,總算是比較清楚整個運算的來龍去脈。

有時間整理出來的話,再拿來分享好了。

簡單的說,covariance matrix計算(x,y,z)數據的變異量以及彼此之間的影響,例如,x軸方向的數據,在y軸數據變動時,所受到的影響,這東西可以從最小平方差的總和,以偏微分取極值的方式導出類似的計算。

接下來這樣想,假設我們已經知道了OBB的三個軸向,我們就可以將所有的頂點(x,y,z),轉成在OBB座標系中的頂點(x',y',z'),而以(x',y',z')所計算得的covariance matrix,兩個不同的軸向(x'與y'或是 y'與z'等等...)之間的互相變異量應該是最小的,滿足這樣條件的三個軸向,就是最佳的OBB軸向。

這些計算一直演算下來,就出現了eigen value problem,所以最後eigen vector又出來幫忙,解決了問題。

好,結論。

結論就是....

數學才是王道啊!!

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

Update: 原來還是看錯了,對應eigen vector的eigen value並不是軸半徑,covariance matrix只用來取得OBB的軸向。