2010年11月25日
Torque 3D V.S. Unity Engine
For serveral monthes, I had been writing a game prototype with a game engine called Torque 3D. Torque is a famous brand with series of game engine solutions including Torque Game Engine Advance (previous generation of Torque 3D), Torque 2D and Torque X (for .NET platform).
Just serveral weeks ago, the owner company of Torque announced they were going to close business and find buyer for Toruqe Engine and its community (link). It's totally not surprising for us, because we've suffer the awful quality of Torque 3D for past 3 mouths. It has a lot of bugs, graphics defects, and poor documentation. We spent 1 mouths to figure out how to work with Torque 3D and another 2 mouths to fix bugs of Torque 3D, and only 1 week actually for game programming. Even we tried so hard to build our game on Torque 3D, the result still made us disapointed and frustrated. The defects of the animation system made our game looks like garbage!!
Therefore, my team started to seek and evaluate other game engines immediately after hearing the news of close of business. We found Unity Engine is easy, efficent, robust, stable in comparison with Torque 3D. Then, we switched our project to it. It's very surprising and exciting that we spent only 3 days to re-build almost everything except level content. Unity Engine is indeed much more excellent than Torque 3D!!!
In this week, with Unity Engine,I've finished implementing many features that I never expect they can be done with Torque 3D. The difference of usability between these two engines are so huge that I want to write some articles to find out what makes an game engine succeed and what makes it fail. Hopefully, I can finish articles soon and post them to my blog.
2010年11月14日
Inheritance, Virtual Method Table, and Memory Layout of Class
Let's start from the bug I encountered. Following is a simple program used to illustrate this bug.
class Base{
public:
int a;
int b;
};
class Derived : public Base{
public:
int c;
virtual void methodA(){
return;
}
};
int main(){
// create derived class
Derived* derived_ptr = new Derived();
// cast derived point to base pointer
Base* base_ptr = static_cast<base*>(derived_ptr);
// delete derived class with base pointer
delete base_ptr;
// exit
return 0;
}
If you run the binary compiled from VC++, run-time exception (i.e. crash) will be definitely raised when deleting the object of Drived. After trying to set up break points, runing code line by line, looking up C++ standard to make sure this is not the bug of compiler, you may be surprised that you just need a virtual destructor for Base.
class Base{
public:
// virtual destructor
virtual ~Base(){}
int a;
int b;
};
If you've already realized the root cause from the solution and the title, you need not to continue reading this article. For those who feel confusing, the following is the explanation with delicate illustrations.
Have a look at this diagram:
This is a diagram showing the object memory layout of class Derived. It includes a continuous sub-section (labelled as blue blocks) that holds the structure of class Base. You would notice there is compiler generated member __vptr, which is the pointer to the virtual method table, in the head of the memory layout of class Derived. Class Base doesn't have __vptr because it doesn't have any virtual table. Therefore, if you cast a pointer from Derived* to class Base* actually result in a shift of 4 bytes (for 32 bits system). That's why the program crashes if you delete the pointer base_ptr. The deleting address is not the address where object created!
Let's see what's happing when the virtual destructor is added to class Base:
Because class Base now have virtual method (it's destructor), the object memory layout of class base must also have a space for __vptr. Therefore, when you access the object of class Derived with the pointer of class Base, the computer will recognize blue blocks as the memory block of the object. Therefore, pointer casting between class Derived and class Base results in no address shift.
This is not the end of the story. The object memory layout gets more complicated when it comes to multiple inheritance. Consider the following program:
class Base{
public:
virtual ~Base(){}
int a;
int b;
};
class Base2{
public:
virtual ~Base2(){}
int a2;
int b2;
};
class Derived : public Base, public Base2{
public:
int c;
virtual void methodA(){
return;
}
};
int main(){
// create derived object
Derived* derived_ptr = new Derived();
// cast Derived pointer to Base pointer
Base* base_ptr = static_cast<base*>(derived_ptr);
// cast Derived pointer to Base2 pointer
Base2* base2_ptr = static_cast<base2*>(derived_ptr);
// delete derived object with base2 pointer
delete base2_ptr;
return 0;
}
The object memory layout of class Derived becomes:
The pointer base2_ptr has 12 bytes shift in comparison with derived_ptr. You might think this program will crash when calling delete with base2_ptr due to address shifting. Unexpectedly, the derived object is correctly deleted, because class Base2 also has virtual destructor, and virtual function mechanism do help address the deletion, thus avoiding free memory in wrong address.
Conclusion
You must declare virtual destructor for a class that is expected to be inherited by other classes. Publisher: Unknown - 星期日, 11月 14, 20102010年10月31日
2010年7月18日
Modified Lubbo's Fan Control
In this forum thread, mizutama revealed an IO method to control macbook fans without killing the process of bootcamp.exe. Based on his demo program WpfFanMon, I integrated his method to Lubbo's Fan Control 0.19.
My modified version of Lubbo's Fan Control 0.19 can be downloaded from
2010年7月6日
Set up WAR version of Alfresco Community 3.3 on ubuntu 10.04
enable 'partner' repository in /etc/apt/sources.list. Otherwise the required package sun-java6-jdk will not be installed
Install required packages:sudo apt-get install mysql-server sun-java6-jdk imagemagick swftools openoffice.org-core openoffice.org-java-common openoffice.org-writer openoffice.org-impress openoffice.org-calc
Set environment variables by add the following lines to /etc/environment:JAVA_HOME="/usr/lib/jvm/java-6-sun/"
JAVA_OPTS="-Xms256m -Xmx1024m -Xss96k -XX:MaxPermSize=512m -server"
Install tomcat6 and mod_jk for apache2 integration:apt-get install tomcat6 libapache2-mod-jk
Configure mysql:- Edit /etc/mysql/my.cnf, add following lines under [mysqld]
[mysqld]
#
# * Basic Settings
#
#
default-character-set = utf8 - Restart mysql:
/etc/init.d/mysql restart
- Create and prepare database for alfresco
Log into MySQL using mysql's root and your password (assigned by the user when installed by apt-get)mysql -u root -p
Input creation commandsmysql> CREATE DATABASE alfresco DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
Note that there is a ';' in the end of each line.
mysql> GRANT ALL PRIVILEGES ON alfresco.* TO alfresco@localhost IDENTIFIED BY 'alfresco';
mysql> GRANT SELECT,LOCK TABLES ON alfresco.* TO alfresco@localhost IDENTIFIED BY 'alfresco';
mysql> FLUSH PRIVILEGES;
mysql> quit;
- Edit /etc/mysql/my.cnf, add following lines under [mysqld]
- Download Afresco Community 3.3 WAR package
wget http://dl.alfresco.com/release/community/build-2860/alfresco-community-war-3.3g.tar.gz
and extract files to afresco_installmkdir alfresco_install
and move the war file to the webapps
tar zxf alfresco-community-war-3.3g.tar.gz -C alfresco_installmv alfresco_install/alfresco.war /var/lib/tomcat6/webapps
chown tomcat6:tomcat6 /var/lib/tomcat6/webapps/alfresco.war
Move alfresco_install/extensions to /var/lib/tomcat6/shared/class/alfrescomv extensions /var/lib/tomcat6/shared/classes/alfresco
and change the owner of /var/lib/tomcat6/shared/classes/alfresco to tomcat6chown tomcat6:tomcat6 /var/lib/tomcat6/shared/classes/alfresco -R
Edit an new file alfresco-global.properties under /var/lib/tomcat6/shared/classes/vi /var/lib/tomcat6/shared/classes/alfresco-global.properties
and add following lines#specify the directory that you want alfresco works on.
and then change its owner
dir.root=/var/alfresco/alf_data
db.name=alfresco
db.username=alfresco
db.password=alfresco
db.host=localhost
db.port=3306
db.driver=org.gjt.mm.mysql.Driver
db.url=jdbc:mysql://localhost/alfresco
hibernate.dialect=org.hibernate.dialect.MySQLInnoDBDialect
ooo.exe=/usr/lib/openoffice/program/soffice
ooo.enabled=true
img.root=/usr
swf.exe=/usr/bin/pdf2swf
cifs.enabled=true
cifs.serverName=dms
cifs.ipv6.enabled=false
cifs.tcpipSMB.port=1445
cifs.netBIOSSMB.namePort=1137
cifs.netBIOSSMB.datagramPort=1138
cifs.netBIOSSMB.sessionPort=1139chown tomcat6:tomcat6 /var/lib/tomcat6/shared/classes/alfresco-global.properties
Create the directories that alfresco works on:mkdir /var/alfresco/alf_data -p
and change their ownerchown tomcat6:tomcat6 /var/alfresco -R
Download JDBC Driver for mysqlwget http://dev.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.13.tar.gz/from/http://mysql.mirror.tw/
and extract the JDBC driver and mv it to /usr/share/tomcat6/libtar zxf mysql-connector-java-5.1.13.tar.gz
mv mysql-connector-java-5.1.13/mysql-connector-java-5.1.13-bin.jar /usr/share/tomcat6/lib
chown tomcat6:tomcat6 /usr/share/tomcat6/lib/mysql-connector-java-5.1.13-bin.jar
Finally, we can make alfresco explorer work by restarting tomcat./etc/init.d/tomcat6 restart
2010年4月28日
Component-Based Game Object System, Part I
前言
在一些較舊的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
2010年4月9日
This is Game Industry, This is Taiwan
我的夢想是寫個好程式,做個好遊戲。但在這個工業,似乎是永遠沒有足夠的資源讓你做「好」一個遊戲。
看著對手,K國的T公司,做出來的產品,既豐富又有趣,一個接一個推上架。我不禁思考:為什麼他們可以又快又好,而我們的品質與內容總得在對方的強勢下求生存。據說,他們是做了又長又久,研發了好幾代,人又多。而這裡,受限於有限的資金,我們必需在及短的時間,很少的人力把遊戲生出來,然後得到限金和利潤。所以,我們可以說,輸了是很合理的嗎?資方或許期待只以5個[人x年]生出來的遊戲,就能和100[人x年]生出來的遊戲平起平坐,我們身為沒有投入資金的勞方而言,有資格抱怨嗎?我們能夠說:「資方TMD投入一個工程師二位美術一個企劃才8個月就肖想能做出像X遊戲一樣賣座的東西!」嗎?究竟是資方太貪婪,還是勞方太怠惰?
我想不負眾望做出好遊戲,所以當感到時程壓力,連續一個月,下班也把程式當晚餐回家寫,周末也花了一半的時間在家工作。很快地,就疲乏了。同事勉勵我說,公司看的是你付出多少,付出多的,以後有賺錢回饋也多。他的勉勵在我腦中回響,我深思。他的意思是有賺錢,你有賣命就有錢分。但賺錢前提是有客戶要買你的遊戲,然而,要客戶買你的遊戲,也要遊戲夠「好」。
我能夠做「好」遊戲,只要給我報酬和時間,報酬能夠延續「以做遊戲為職志熱情」,時間(泛指人力乘以天數)是始於成就產品不可超越的物理極限。報酬可以寄托未來,而不要求,但後者無論卻如何不能缺乏。所以在資方不願付出時間的情況下,只憑勞方打拼真的能做出「好」的遊戲嗎?能做出賺錢的遊戲嗎?我給個很大的問號。或許接受了同事的勉勵後,到頭來還是因為沒有時間而失敗了,遊戲夢最後還是一場空。
我想這是一個惡性循環,資方越不願意投入耐心和時間,產品就愈可能失敗,而產品愈可能失敗,資方就愈不願意投入時間給勞方的報酬也愈低。要打破這循環,我想不能期望短視保守的資方承擔風險,畢竟它是商人,所以在這個產業永遠是對遊戲有熱情的勞方先付出,先承擔風險。你也許會問什麼風險?勞方付出了歲月、健康和生活,期望有一天自己熱愛的事物會帶來豐厚的報酬,如果失敗了,沒了活力,也沒有其它領域的經歷,前份工作的薪水又如此之低,下場我想是很淒涼吧。
我們,遊戲人生的追隨者,是不是該拿起尚未失去光環但保存期限快過的畢業證書,趁我們的劍刃還沒磨頓,去找個外商,圖個起碼在另一半和親人面前走路有風的月薪?寫到最後,被自己的文字弄得頹喪。雖然,我心中仍有一絲的希望,我想再寫幾個句子來個積極上進的結尾,但我發現我無法,彷彿是把那微弱的光明說出來,它就煙消雲散。
Publisher: Unknown - 星期五, 4月 09, 2010