ioquake3 原始碼編譯初步

id Software 於 1999 年所推出的《雷神之鎚 III》(Quake III Arena,以下簡稱 Q3A)可說是電玩遊戲開發的經典教材,其完整原始碼於 2005 年釋出。至於 ioquake3 則是由開發者社群所發起的 Q3A 後續維護專案,其目的是在不影響原本遊戲內容的前提下,修正遊戲臭蟲、加入新技術並且持續支援新的平台。過去已經有多款遊戲是以 ioquake3 的原始碼為基礎所開發的,例如 OpenArenaUrban TerrorWorld of Padman 等。

 

由於 ioquake3 才是有在持續維護的原始碼版本,因此無論是純粹想研究遊戲原始碼、或者想要開發出基於 Q3A 引擎的新遊戲,都應該直接從 ioquake3 的原始碼著手。以下本文將說明如何在 Linux 64bit 平台(Fedora 17 x86_64)下載、編譯 ioquake3,以及已編譯檔案的發佈與執行。最後還會包含一小段的原始碼修改範例。

 

 

下載 ioquake3

目前 ioquake3 的原始碼已經轉移到 GitHub,因此請先使用 yum 來安裝 Git 版本控制系統:

  • yum install git

 

然後使用 Git 下載完整原始碼:

  • git clone git://github.com/ioquake/ioq3.git

 

完成後,使用者目錄便會出現已下載完畢的 ioq3 目錄。

 

Q3A 原始碼簡介

Q3A 的原始碼皆放置在 ioq3/code 目錄內,其原始碼若以二分法來解釋可以分成兩大部份:engine code 與 game code。engine code 所涉及的便是遊戲程式最根本的、與作業系統互動的部份,包含繪圖、聲音、網路、輸出入控制等。而 game code 所涉及的便是遊戲模式、規則、邏輯與外觀等。Q3A 的 game code 是分佈在以下四個子目錄裡:cgame、game、q3_ui 以及 ui,而這些 game code 並不像 engine code 是編譯到 Q3A 的執行檔裡,而是編譯到 Q3A 特有的虛擬機器 QVM 裡。

 

認識 QVM(Quake Virtual Machine)

QVM 顧名思義指的就是「雷神之鎚虛擬機器」,若把 Q3A 的遊戲引擎比喻成作業系統,那麼 QVM 就是運行在其上的應用程式。採用 QVM 的好處一方面是可以在無需修改主程式的情況下修改遊戲內容,有利於擴充模組(modification)的開發,另一方面也提高了穩定性與安全性。並且 QVM 是跨平台的,編譯好的 QVM 並不需要重新編譯就可以轉移到不同平台上,由 Q3A 主程式進行解譯。

 

Q3A 的 QVM 分為三部份:game、cgame 和 ui,而這三個 VM 分別負責了伺服端程式、客戶端程式以及選單介面的控制,前面所提到的 game code 最終便是編譯到這三個 VM 裡。

 

第一次編譯

編譯 ioquake3 原始碼所需的函式庫為 SDLOpenAL,SDL 和 OpenAL 的開發函式庫可以在 Fedora 上使用 yum 安裝:

  • yum install SDL-devel openal-soft-devel

 

安裝完所需的開發函式庫後就可以進行編譯,ioq3 目錄下已經有設置好的 Makefile,直接在該目錄下執行編譯即可:

  • cd ioq3
  • make

 

如果所使用的系統上沒有 OpenAL 開發函式庫,也可以在 make 時加入額外參數以停用 OpenAL:

  • make USE_OPENAL=0

 

停用 OpenAL 一樣會有聲音,只是如此一來就沒有多聲道音效的支援。另外在 make 時也可以選擇只編譯 game code、也就是 QVM 的部份:

  • make BUILD_GAME_QVM=1

 

這個參數等會還會再用到,因為 DR 將會示範只修改 game code 的方式。如果所有的程式碼編譯時都沒有發生錯誤,完成編譯的檔案會放置在 ioq3/build/release-linux-x86_64 目錄下。以下 DR 會挑重點來說明至這些檔案,首先是執行檔的部份:

  • ioquake3.x86_64 - 遊戲執行檔
  • ioq3ded.x86_64 - 伺服器專用執行檔

 

接著是 QVM 的部份,由於 Q3A 事實上還支援將 QVM 編譯成對應特定平台的動態函式庫(*.dll、*.so),因此預設會產生兩種格式的 QVM 在以下的子目錄裡:

  • baseq3/qagamex86_64.so
  • baseq3/cgamex86_64.so
  • baseq3/uix86_64.so
  • baseq3/vm/qagame.qvm
  • baseq3/vm/cgame.qvm
  • baseq3/vm/ui.qvm

 

使用動態函式庫的好處是可以加快載入速度,不過這點速度差異對於現今的硬體效能而言可能不甚重要。再者動態函式庫是沒有辦法跨平台的,若想要很方便的將修改過的 game code 拿到各平台上的 Q3A 執行,使用 *.qvm 會是比較好的作法。

 

 

第一次執行

要能夠執行 Q3A 必須具備三項條件:執行檔、資料檔與 QVM,首先執行檔並沒有要求固定的存放路徑,想放哪裡都可以。而資料檔以 Linux 平台來說,預設的讀取路徑是使用者目錄下的 .q3a/baseq3,裡頭必須要存放從 pak0.pk3 到 pak8.pk3 共九個資料檔才能執行,其中 pak0.pk3 可以在 Q3A 的原版遊戲光碟裡找到,至於剩餘的檔案(其實就是更新檔)則可以在 ioquake3 網站上取得(Patch Data)。

 

Q3A 是開放原始碼,但資料檔案可不是,如果手上沒有 Q3A 的話,可以考慮至 Steam 購買。

 

QVM 的處理有兩種方式,第一種是將動態函式庫(*.so)全數複製到 baseq3 目錄裡,然後在執行 Q3A  時加入額外參數就能以動態函式庫的方式載入:

  • ./ioquak3.x86_64 +set vm_cgame 0 +set vm_game 0 +set vm_ui 0

 

另一種則是 DR 比較建議的方式,就是將 *.qvm 包入遊戲的資料檔裡,Q3A 所使用的 *.pk3 資料檔其實就是 ZIP 壓縮檔,只是副檔名換了而已。因此其實只要按照 Q3A 的檔案讀取結構,建立一個 ZIP 壓縮檔,將 vm 目錄整個包進去,也就是以下檔案:

  • vm/qagame.qvm
  • vm/cgame.qvm
  • vm/ui.qvm

 

然後重新命名為 pak9.pk3,放到 baseq3 目錄裡,執行 Q3A 時不用加任何參數就會自動載入自行編譯的 QVM:

  • ./ioquake3.x86_64

 

清楚了 Q3A 編譯、發佈與執行的流程後,接著就可以進行一些初步的原始碼修改。

 

原始碼修改範例

以下 DR 將舉例示範針對 game code 進行遊戲內容的修改,設定的需求如下:

  1. 讓所有玩家預設便取得所有武器
  2. 所有玩家以亂數擲骰的方式獲得彈藥和能力加強(power-up)物品
  3. 死去或離線玩家將不會掉落武器彈藥

 

首先編輯 game/g_client.c,找到 ClientSpawn() 函式,在裡頭可以看到所有玩家預設已經有了一把機槍:

client->ps.stats[STAT_WEAPONS] = ( 1 << WP_MACHINEGUN );

 

就給它多加幾把上去吧:

client->ps.stats[STAT_WEAPONS] = ( 1 << WP_MACHINEGUN );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_SHOTGUN );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_ROCKET_LAUNCHER );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GRENADE_LAUNCHER );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_LIGHTNING );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_RAILGUN );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_PLASMAGUN );
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_BFG );

 

接著編輯 game/g_active.c,找到 ClientTimerActions() 函式,裡頭有一段讓所有玩家每秒執行一次動作的段落剛好是可以運用的部份:

	gclient_t	*client;
	int			quantity;
#ifdef MISSIONPACK
	int			maxHealth;
#endif

	client = ent->client;
	client->timeResidual += msec;

	while ( client->timeResidual >= 1000 ) {
		client->timeResidual -= 1000;

 

DR 要在裡頭加入每秒執行一次亂數骰的機制,並且依據亂數骰的結果給予該名玩家不同種類的彈藥或物品。C 語言的亂數函式就是 rand(),然而這邊要注意的是:所有編入 QVM 的程式碼、也就是 game code 並無法直接取用外部函式庫,包含 C 標準函式庫(C Standard Library)都不行,而僅限於 engine 所給予的函式,所以在此情況下有兩種解法:

  1. 修改 engine code,引入並包裝所需的外部函式,讓 game code 取用
  2. 搜尋 game code 的可用函式以達成所需的功能

 

以後者的作法而言,DR 找到了 random() 函式,不過它所輸出的亂數是小於 1 的浮點數,所以在使用上還要做一點處理。另外由於 printf() 函式當然也是無法使用,因此 DR 使用 engine 所提供的 Com_Printf() 函式來做變數的檢查,Com_Printf() 的輸出會顯示在遊戲畫面的左上角。完成後的程式碼如下:

	gclient_t	*client;
	int			quantity;
	int dice;
#ifdef MISSIONPACK
	int			maxHealth;
#endif

	client = ent->client;
	client->timeResidual += msec;

	while ( client->timeResidual >= 1000 ) {
		client->timeResidual -= 1000;
		dice = (int)(random() * 10000000) % 20;
		Com_Printf("dice: %d\n", dice);
		switch(dice) {
			case 1: client->ps.ammo[WP_MACHINEGUN] += 10; break;
			case 2: client->ps.ammo[WP_SHOTGUN] += 1; break;
			case 3: client->ps.ammo[WP_ROCKET_LAUNCHER] += 1; break;
			case 4: client->ps.ammo[WP_GRENADE_LAUNCHER] += 1; break;
			case 5: client->ps.ammo[WP_LIGHTNING] += 3; break;
			case 6: client->ps.ammo[WP_RAILGUN] += 1; break;
			case 7: client->ps.ammo[WP_PLASMAGUN] += 3; break;
			case 8: client->ps.ammo[WP_BFG] += 1; break;
			case 11: client->ps.powerups[PW_BATTLESUIT] += 3000; break;
			case 12: client->ps.powerups[PW_FLIGHT] += 10000; break;
			case 13: client->ps.powerups[PW_HASTE] += 8000; break;
			case 14: client->ps.powerups[PW_INVIS] += 10000; break;
			case 15: client->ps.powerups[PW_QUAD] += 4000; break;
			case 16: client->ps.powerups[PW_REGEN] += 4000; break;
			default: break;
		}

 

武器彈藥部份,10 表示增加 10 發,而物品部份,3000 表示增加 3 秒。

 

最後,要讓死去或離線玩家不會掉落武器彈藥,這部份很單純,就是將相關的函式註解掉即可。先在 game/g_combat.c 找到 player_die() 函式,將 TossClientItems() 函式註解掉:

	if ( !( contents & CONTENTS_NODROP )) {
		//TossClientItems( self );
	}

 

然後在 game/g_client.c 找到 ClientDisconnect() 函式,將同樣的函式註解掉就完成了:

	if ( ent->client->pers.connected == CON_CONNECTED 
		&& ent->client->sess.sessionTeam != TEAM_SPECTATOR ) {
		tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT );
		tent->s.clientNum = ent->s.clientNum;

		// They don't get to take powerups with them!
		// Especially important for stuff like CTF flags
		//TossClientItems( ent );

 

第二次編譯與執行

將所需的修改儲存後,由於只有修改到 game code 的部份,因此編譯時也只需要針對 QVM 重新編譯就好:

  • make BUILD_GAME_QVM=1

 

然後將再次編譯好的 *.qvm 加入到 pak9.pk3,覆蓋原本的檔案,就可以執行修改過的遊戲了。至於 DR 的測試方式是開一張地圖、加入 Bot 彼此對戰來進行測試,而這個流程可以用簡單的設定檔來快速執行,先在遊戲的 baseq3 目錄下新增 test.cfg,並寫入以下內容:

map q3dm1
fraglimit 0
timelimit 0
wait 100
team spectator
addbot doom
addbot grunt
addbot razor
addbot mynx
addbot xaero
 
 
儲存後,在執行 Q3A 時加入以下參數:
  • ./ioquak3.x86_64 +exec test.cfg

 

這樣 Q3A 便會按照設定檔中的指令來啟動遊戲,或者在遊戲執行後以按下「~」鍵的方式開啟命令行執行:/exec test.cfg 也會有同樣的效果。

 

最後 DR 於 YouTube 上傳了一小段測試影片以供參考,裡頭包含了以上所示範的程式碼以及一些未在本文中提及的細微修改(不知是否看得出來?),此外影片中所使用的極簡地圖則是 DR 自己用 GtkRadiant 關卡編輯器製作的。

 

延伸閱讀: