摘要:腳本技術(shù)在游戲引擎中的應(yīng)用是近年來(lái)游戲開(kāi)發(fā)一個(gè)新的趨勢(shì)。本文探討了腳本技術(shù)在游戲引擎中的作用,并以一種腳本語(yǔ)言Lua為例,詳細(xì)分析了腳本與引擎通信的原理。
關(guān)鍵詞:游戲引擎;腳本;集成;Lua
中圖分類號(hào):TP311文獻(xiàn)標(biāo)識(shí)碼:A文章編號(hào):1009-3044(2008)19-30167-03
The Application of Scripting Technology in Game Engine
YU Zao-bo
(College of Software Engineer, Southeast University, Nanjing 210018, China)
Abstract: Scripting Technology applied in game Engine is a new trend in nowadays. In this paper, we described the importance of Scripting Technology in Game Engine, and analysis the principle of communication between script and engine with the Lua Language.
Key words: Game Engine; Script; Integration; Lua
1 引言
游戲引擎的產(chǎn)生從根本上講是為了代碼的可復(fù)用,早期的游戲開(kāi)發(fā)效率很低,幾乎每款游戲都要從頭編寫(xiě)代碼,造成了大量的重復(fù)勞動(dòng); 然而每款類似題材的游戲基本框架是相對(duì)穩(wěn)定的,這種特性使得可以把這部分相對(duì)穩(wěn)定的代碼從整個(gè)游戲中分離出來(lái), 并且可以在多款游戲中重復(fù)使用。 現(xiàn)在的商業(yè)游戲99%以上都是這樣工作的:首先開(kāi)發(fā)一個(gè)游戲引擎,接著在虛擬機(jī)的基礎(chǔ)上使用腳本語(yǔ)言系統(tǒng)創(chuàng)建一個(gè)引擎接口。游戲開(kāi)發(fā)者,甚至游戲設(shè)計(jì)者,用腳本語(yǔ)言來(lái)創(chuàng)建游戲的真正邏輯以及整個(gè)游戲的行為。
2 游戲引擎
游戲引擎的發(fā)展到現(xiàn)在已經(jīng)日益成熟,游戲的畫(huà)面達(dá)到了一個(gè)瓶頸的高度了,引擎以后的發(fā)展方向難以朝著圖形圖像的改良而進(jìn)行下去了。然而,引擎的作用絕不僅僅局限在游戲畫(huà)面中,它會(huì)直接影響到游戲的整體風(fēng)格。所以游戲開(kāi)發(fā)者不得不從其它方面尋求突破,目前業(yè)界主要在關(guān)注以下兩個(gè)領(lǐng)域:
1) 在引擎中引入腳本的技術(shù),這種技術(shù)可以讓游戲以合理的故事來(lái)觸動(dòng)游戲整體架構(gòu)上的變化,使得玩家可以真實(shí)的體驗(yàn)到游戲情節(jié)的發(fā)展;
2) 在人工智能算法上的改進(jìn),在游戲中,敵人的行動(dòng)與以前相類似的游戲來(lái)看,他們明顯有了更多狡猾的為,而不再只是單純的沖向玩家所在的位置。
這兩方面的特點(diǎn)明顯的突破了以往的引擎架構(gòu)。現(xiàn)在越來(lái)越多的游戲開(kāi)發(fā)者開(kāi)始將游戲中的重點(diǎn)有單純的視覺(jué)效果逐漸的轉(zhuǎn)變成具有更豐富變化性的游戲內(nèi)容,這也成功的說(shuō)明了故事內(nèi)容與人工智能對(duì)于游戲的重要性。是否能夠支持更好的游戲故事內(nèi)涵和敵人的反映特性己成為衡量引擎優(yōu)劣的另一項(xiàng)標(biāo)準(zhǔn)。
3 腳本技術(shù)
3.1 概述
腳本是一種通??梢跃庉嫼瓦\(yùn)行、具有極高抽象級(jí)別的編程語(yǔ)言,而腳本技術(shù)就是與此相關(guān)的技術(shù)的總稱。由于其強(qiáng)大的自定義功能,腳本技術(shù)正在被越來(lái)越多的軟件供應(yīng)商使用。
腳本語(yǔ)言作為一類開(kāi)發(fā)語(yǔ)言,主要有以下優(yōu)點(diǎn):
1) 快速開(kāi)發(fā)。它們大大縮短了“開(kāi)發(fā)、部署、測(cè)試、調(diào)試”周期;
2) 部署簡(jiǎn)便。大多提供即時(shí)部署能力,而無(wú)需花費(fèi)大量時(shí)間在編譯和打包周期上;
3) 與已有技術(shù)集成。它們大都構(gòu)建在已有的組件技術(shù),以便有效重復(fù)利用現(xiàn)有代碼;
4) 易于學(xué)習(xí)和使用。技術(shù)門檻很低,可以輕松找到大量的使用者;
5) 動(dòng)態(tài)代碼。腳本語(yǔ)言能夠被即時(shí)生成和執(zhí)行,這在某些應(yīng)用程序中是非常必要和有用的高級(jí)特性。
3.2 腳本在引擎中的作用
實(shí)現(xiàn)腳本編程系統(tǒng)能夠的最根本原因是要避免硬編碼,將游戲內(nèi)容與游戲引擎相分離。這樣,就可以在不需要重新編譯整個(gè)工程的情況下調(diào)整、測(cè)試和修改游戲運(yùn)行的機(jī)制和特性。這種模塊化的結(jié)構(gòu)促成了游戲開(kāi)發(fā)工作量的劃分,游戲編程人員負(fù)責(zé)引擎等核心技術(shù)的開(kāi)發(fā),而游戲設(shè)計(jì)者可以專注游戲本身的邏輯與策劃。
從系統(tǒng)架構(gòu)方面講, 使用腳本可以減少與底層的耦合度。把游戲邏輯相關(guān)的部分放進(jìn)腳本中,這部分模塊實(shí)際上可以被多個(gè)游戲項(xiàng)目復(fù)用。因?yàn)槟_本是基于虛擬機(jī)的,只要運(yùn)行時(shí)環(huán)境接口一致,腳本部分代碼便可以實(shí)現(xiàn)真正意義上的跨平臺(tái),這一點(diǎn)與java語(yǔ)言很相似。另外,腳本語(yǔ)言相對(duì)C++來(lái)說(shuō)要簡(jiǎn)單的多,經(jīng)過(guò)簡(jiǎn)單的培訓(xùn),甚至策劃人員也可以參與編寫(xiě),大大降低了開(kāi)發(fā)門檻。
4 腳本系統(tǒng)與游戲引擎的集成
4.1 原理分析
集成(Integration)是將兩個(gè)或者更多個(gè)分離的、通常情況下不相關(guān)的實(shí)體連接在一起,使得它們?yōu)榱艘粋€(gè)共同的目標(biāo)相互通信、協(xié)調(diào)工作。將兩個(gè)實(shí)體集成最大的挑戰(zhàn)就是如何在它們之間建立某種類型的通道,從而使得兩者可以進(jìn)行方便而又可靠的通信。
Lua是基于關(guān)聯(lián)數(shù)組和可擴(kuò)展語(yǔ)法結(jié)構(gòu)設(shè)計(jì)的語(yǔ)言,具有變量無(wú)類型、動(dòng)態(tài)定義類型、面向?qū)ο蠼Y(jié)構(gòu)、編譯產(chǎn)生中間代碼和內(nèi)存自動(dòng)回收等特點(diǎn),常被作為一種腳本嵌入于其它主系統(tǒng)中。但是Lua作為一種腳本語(yǔ)言,不是直接編譯為機(jī)器碼,而是在運(yùn)行時(shí)通過(guò)運(yùn)行環(huán)境或虛擬機(jī)執(zhí)行源代碼或中間代碼。相比較編寫(xiě)引擎使用的C/C++語(yǔ)言代碼,引擎與腳木的運(yùn)行環(huán)境不同,從而導(dǎo)致了它們之間不能直接通信,所以集成的關(guān)鍵在于要在兩者間建立一個(gè)抽象層,也就是接口,能夠解釋和傳遞它們的輸入輸出。一般來(lái)說(shuō),腳本系統(tǒng)的運(yùn)行是通過(guò)使用靜態(tài)庫(kù)的方式實(shí)現(xiàn)的,這個(gè)庫(kù)包括了兩個(gè)關(guān)鍵部分:第一部分是運(yùn)行環(huán)境,也稱為虛擬機(jī),用來(lái)把腳本語(yǔ)言翻譯成機(jī)器語(yǔ)言;另一部分是與主程序連接的函數(shù)接口,提供把腳本嵌入到主程序的接口。它與主程序的關(guān)系如圖1所示:
■
圖1 lua與c通信
由圖1可以看到,引擎與lua腳本之間的調(diào)用是雙向的,即lua可以調(diào)用引擎函數(shù),引擎也可以調(diào)用lua函數(shù)。Lua中用狀態(tài)(State)這個(gè)概念描述運(yùn)行時(shí)環(huán)境相關(guān)的某個(gè)具體實(shí)例的信息,每個(gè)狀態(tài)在任意時(shí)刻都可以包含一個(gè)腳本,這個(gè)腳本已經(jīng)被裝載到內(nèi)存當(dāng)中以供使用。同時(shí),由于lua運(yùn)行時(shí)環(huán)境是運(yùn)行在引擎上的,也就是說(shuō)引擎能夠訪問(wèn)lua的運(yùn)行時(shí)環(huán)境,因此lua與引擎都能操作運(yùn)行時(shí)環(huán)境中的堆棧。把棧作為兩者通信的中間層,通過(guò)把全局變量、函數(shù)引用、參數(shù)、返回值等壓入堆棧,以達(dá)到信息共享的作用。如圖2所示:
■
圖2 Lua堆棧
4.2 Lua調(diào)用引擎函數(shù)
為了使在引擎中定義的函數(shù)能夠被Lua腳本調(diào)用,需要傳遞一個(gè)函數(shù)指針至Lua的運(yùn)行環(huán)境,Lua提供了注冊(cè)函數(shù)名的接口:
lua_register (lua_State *pLuaState, const char *pstrFunctionName, lua_CFunction pFunc);
只需要提供一個(gè)函數(shù)名稱字符串,當(dāng)前函數(shù)指針以及當(dāng)前函數(shù)應(yīng)導(dǎo)出到的具體Lua狀態(tài),函數(shù)lua_register()就會(huì)注冊(cè)這個(gè)函數(shù),這就是游戲腳本可以像訪問(wèn)其它函數(shù)一樣來(lái)調(diào)用這個(gè)函數(shù)的原因。需要注意的是,如果第三個(gè)參數(shù)pFunc未被導(dǎo)出,會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。 正確的定義格式應(yīng)該是這樣:
int FuncName(lua_State *pLuaState);
由于Lua腳本對(duì)函數(shù)參數(shù)及返回值處理與C語(yǔ)言不同,Lua是把參數(shù)和返回值都?jí)喝氘?dāng)前State棧中,通過(guò)訪問(wèn)棧中的變量達(dá)到傳遞的作用,所以在lua_register()中沒(méi)有對(duì)函數(shù)參數(shù)、返回值個(gè)數(shù)的說(shuō)明。在函數(shù)調(diào)用時(shí),所有對(duì)堆棧的訪問(wèn)都是相對(duì)與索引值1開(kāi)始的,參數(shù)個(gè)數(shù)可以用lua_gettop()函數(shù)獲得, 并用lua_to*()訪問(wèn)每一種類型的變量。例如一個(gè)函數(shù)參數(shù)表如下:
(int x, float y, string z)
需要采用下面的方式讀取這3個(gè)參數(shù):
int x = (int) lua_tonumber(pLuaState, 1);
float y = lua_tonumber(pLuaState, 2);
char *z = lua_tostring(pLuaState, 3);
函數(shù)返回值采用相反的方式進(jìn)行返回,需要在C函數(shù)返回前將這些數(shù)值壓入堆棧。假設(shè)要返回3個(gè)數(shù)值2、4、6,則代碼應(yīng)該如下:
lua_pushnumber(pLuaState, 2);
lua_pushnumber(pLuaState, 4);
lua_pushnumber(pLuaState, 6);
return 3;
注意由于lua可以返回多個(gè)值,所以最后要加上return 3。
4.3 引擎調(diào)用Lua函數(shù)
引擎調(diào)用Lua函數(shù)正好是與上節(jié)相反的過(guò)程,在lua中所有函數(shù)都可以看作是全局范圍的,沒(méi)有作用域的概念。當(dāng)調(diào)用一個(gè)函數(shù)時(shí),需要完成的第一件事就是將一個(gè)對(duì)該函數(shù)的引用壓入堆棧,可以使用lua_getglobal()來(lái)完成這個(gè)工作:
lua_getglobal(pLuaState, \"FuncName\");
在這里,F(xiàn)uncName是一個(gè)字符串值,在腳本內(nèi)部對(duì)應(yīng)于函數(shù)的名稱。緊接著要將函數(shù)參數(shù)壓入堆棧,對(duì)于函數(shù)原型為:
function FuncName(int Param1, string Param2);
假設(shè)按照以下方式調(diào)用這個(gè)函數(shù):
FuncName(256, \"Hello!\");
參數(shù)應(yīng)該以下面的方式壓入堆棧:
lua_pushnumber(pLuaState, 256);
lua_pushstring(pLuaState, \"Hello!\");
然后采用如下方式來(lái)調(diào)用函數(shù):
Lua_call(pLuaState, 2, 1);
其中,2表示參數(shù)個(gè)數(shù)為2個(gè),1表示返回值個(gè)數(shù)為1個(gè)。
最后是獲取腳本函數(shù)返回值,該返回值同樣是保存在堆棧之中。假設(shè)腳本函數(shù)返回一個(gè)整形值,那么應(yīng)該用以下代碼獲取這個(gè)結(jié)果:
int iResult = (int) lua_tonumber(pLuaState, 1);
lua_pop(pLuaState, 1);
即取出棧中索引1的雙精度值并強(qiáng)制轉(zhuǎn)換為整形, 然后從棧中刪除該值。
5 結(jié)束結(jié)
本篇文章探討了腳本技術(shù)在游戲引擎中的地位與作用,并從原理層面分析了腳本與引擎集成的可行性,最后結(jié)合Lua這種腳本語(yǔ)言詳細(xì)介紹了如何實(shí)現(xiàn)游戲引擎與腳本系統(tǒng)間的通信。腳本技術(shù)在游戲引擎中的應(yīng)用是應(yīng)游戲產(chǎn)業(yè)發(fā)展而產(chǎn)生的,它的發(fā)展大大提高了游戲開(kāi)發(fā)的靈活性與高效性,也必然會(huì)得到越來(lái)越多的關(guān)注。
參考文獻(xiàn):
[1] Alex Varanese. Game Scripting Mastery[M]. 清華大學(xué)出版社.
[2] Diego Garces. 腳本語(yǔ)言總述 游戲編程精粹6[M].
[3] 云風(fēng). 游戲之旅——我的編程感悟.電子工業(yè)出版社.
[4] Roberto Ierusalimschy. Programming in Lua[EB/OL].http://www.lua.org 2005.
[5] 房曉溪. 游戲引擎教程[M].中國(guó)水利水電出版社.
注:本文中所涉及到的圖表、注解、公式等內(nèi)容請(qǐng)以PDF格式閱讀原文