遊戲開發中的人工智慧(八):描述式 AI 及描述引擎
本文內容:程式設計師通常只寫描述引擎,而由設計者使用工具建立內容和定義 AI。本章探討一些開發人員把描述系統應用在遊戲中的技巧,以及他們所得到的益處。
描述式 AI 及描述引擎
本章討論某些技巧,讓你把描述系統應用到遊戲軟體 AI 的問題上,以及這樣做以後所能獲得的好處。
從最基本的層次上來看,你可以把描述機制想象成非常簡單的程式語言,專門為與遊戲問題相關的特定工作而量身打造。描述機制可以說是遊戲開發過程中,不可缺少的一部分,因為這可以讓遊戲設計師,而不是遊戲程式設計師,撰寫出遊戲,並予以精細化。玩家也可以利用描述語言,建立或修改其所處的遊戲世界或登記。再進一步的話,你可以在超大型多人線上角色扮演遊戲(MMORG)中使用描述系統,當人們實際在玩遊戲時,就能改變遊戲的行為。
實現描述系統時,可以採用好幾種手段。例如,精緻的描述系統,可以讓實際所用的遊戲引擎和現有的描述語言(例如 Lua 或 Python)銜接起來。有些遊戲會建立專用的描述語言,專門設計處理個別遊戲的需求。雖然有時候利用這些方法比較方便,但是,讓遊戲分析包含描述命令的標準文字檔案,會比較簡單。如果採用這種手段,你就可以用任何標準的文字編輯器,建立指令碼。在實際遊戲中,當遊戲開始時或者在某些特定時刻,可以讀取指令碼,並予以分析。例如,當玩家實際進入城堡時,控制城堡內的生物或事件的指令碼,就能被讀進來並進行分析。
在遊戲軟體 AI 的範圍裡,你可以用描述機制改變對手的屬性、行為、響應方式以及遊戲事件。本章要解釋的就是這些用法。
描述機制技巧
本章我們要建立簡單的描述命令,並將之儲存在標準文字檔案內。我們想避開復雜語言的分析器,就要慎重地選擇詞彙,才能令人輕鬆的讀寫指令碼。即,我們所用的字詞,要正確地反映指令碼要修改的遊戲的哪個方面。
描述對手屬性
利用某種描述機制,指定每個 AI 對手的所有基本屬性。這是很常見也很有益處的做法,這會讓我們在開發和測試過程中,能夠輕易調整 AI 對手。如果把所有重要資料都直接寫程序序裡,即使是最基本的修改,也必須得重新編譯。
一般而言,我們可以描述對手的屬性,比如智慧、速率、強度、膽量以及魔法能力。實際上,可以描述的屬性型別或數量是沒有限制的,真正的決定因素是你正在開發的遊戲是什麼型別的。例如,有較高智慧的 AI,和較低智慧的 AI 相比,行為就會不同。較高智慧的 AI ,會採用比較高階的路徑尋找演算法追蹤玩家;而較低智慧的 AI,則會在試著走向玩家時,很容易被困住。
例8-1 是 設定遊戲屬性的基本指令碼。
//例8-1:設定屬性的基本指令碼
CREATURE=1;
INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;
END;
此例中,我們的指令碼分析器必須編譯五個命令。第一個是 CREATURE,指的是要設定哪一個 AI 作為玩家的對手。下面三個分別是 INTELLIGENCE 智力、STRENGTH 強壯、SPEED 速度,都是實際設定的屬性。最後的命令 END,是通知指令碼分析器,這個生物已經設定完成。
指令碼的基本分析
我們已經介紹了基本屬性指令碼是怎樣的,如例8-1。我們打算進一步探索遊戲如何讀取指令碼並予以分析。舉個例子,我們要以基本指令碼設定巨人的某些屬性。建立一個名叫 Troll Settings.txt 的文字檔案。例8-3 是巨人設定檔案的內容。
//例8-3:設定屬性的基本指令碼
INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;
例8-3 是一個簡單的範例,只為生物設定了3個生物屬性。我們要編寫一個程式,以便能夠輕易地增加其他屬性,我們打算編寫自己的指令碼分析器,使其搜尋指定檔案,找出特定的關鍵字,並返回與該關鍵字相關的值。例8-4 顯示了在實際遊戲程式中的內容。
//例8-4:設定屬性的基本指令碼
intelligence[kTroll] = fi_GetData(Troll Settings.txt,"INTELLIGENCE");
strength[kTroll] = fi_GetData(Troll Settings.txt,"STRENGTH");
speed[kTroll] = fi_GetData(Troll Settings.txt,"SPEED");
在例8-4 中,這三個假想的陣列,可以儲存生物屬性。此時我們通過從名為 Troll Settings.txt 的外部檔案中載入生物屬性值。fi_GetData( ) 函式會檢查這個外部檔案,直到找到指定的關鍵詞,然後,返回與該關鍵字相關的值。這樣,遊戲設計師可以調整生物設定值,而不需在每次修改之後,都得重新編譯程式程式碼。
接下來詳細看一下 從指令碼中讀取資料,即 fi_GetData( ) 函式,如例8-5 所示。
//例8-5:從指令碼讀取資料
int fi_GetData(char filename[kStringLength],char searchFor[kStringLength])
{
FILE *dataStream;
char inStr[kStringLength];
char rinStr[kStringLength];
char value[kStringLength];
long ivalue;
int i;
int j;
dataStream=fopen(filename,"r");
if(dataStream != NULL)
{
while(!feof(dataStream))
{
if(!fgets(rinStr,kStringLength,dataStream))
{
fclose(dataStream);
return 0;
}
j=0;
strcpy(inStr,"");
for(i=0;i<strlen(rinStr);i++)
{
if(rinStr[i]!= ' ')
{
inStr[j]=rinStr[i];
inStr[j+1]='\0';
j++;
}
}
if(strncmp(searchFor,inStr,strlen(searchFor))==0)
{
j=0;
for(i=strlen(searchFor);i<kStringLength;i++)
{
if(inStr[i]==';')
break;
value[j]=inStr[i];
value[j+1]='\0';
j++;
}
StringToNumber(value,&ivalue);
fclose(dataStream);
return ((int)ivalue);
}
}
fclose(dataStream);
return 0;
}
return 0;
}
例8-5 中的函式一開始是接受兩個字串引數。第一個引數是指定要搜尋的指令碼名稱,而第二個是要搜尋的詞彙。然後,這個函式會以指定的檔名開啟指令碼檔案。一旦檔案打開了,這個函式就開始檢查指令碼檔案。一次檢查一行文字,每一行都會被當做字串而讀進來。
注意,每一行都會讀進變數 rinStr 中,再立即複製到 inStr 中,但會去掉空白。
我們利用字串變數 searchFor 把要搜尋的詞傳給 fi_GetData( ) 函式。此時在,在這個函式裡,我們用 C 函式 strncmp( ) 搜尋 inStr,以期能找到所要搜尋的詞。
如果要搜尋的詞沒有找到,這個函式就會繼續讀取指令碼檔案裡的下一行文字。如果找到了,就會進入新的迴圈,把 inStr 變數中含有該屬性值的部分,複製到名為 value 的新字串中。接著再呼叫外部函式 StringToNumber( ),把這個字串轉換成整數值,然後,fi_GetData( ) 函式就返回 ivalue 的值。
描述對手行為
直接影響對手的行為是描述機制在遊戲軟體 AI 中最常用的方式之一。
描述行為可以讓我們直接操縱 AI 對手的行為。我們需要採取某種方式讓指令碼能看懂遊戲世界和檢查條件,以改變 AI 行為。為了做到這一點,我們可以新增預先定義好的全域性變數到我們的描述系統裡。實際的遊戲引擎會替這些變數賦值,而不是由描述語言來指定。
例如,在我們的描述系統裡,我們可能有一個全域性布林變數,名叫PlayerArmed,會讓膽小的巨人只敢去攻擊沒有武裝的對手,如例8-6所示。
//例8-6:基本行為指令碼
if(PlayerArmed==TRUE)
BEGIN
DOFlee();
END
ELSE
BEGIN
DOAttack();
END
在例8-6 中,指令碼沒有替 PlayerArmed 賦值,此變數是代表遊戲引擎裡的某個值。遊戲引擎將評估此指令碼,並把此行為連線到膽小的巨人身上。
描述行為的另一個方面是 AI 角色的移動。我們可以在描述系統中應用在第三章中提到的移動模式。之前在第三章中,我們直接將移動模式寫程序序程式碼中,如果每次做出較小的修改之後都得重新編譯。圖8-1 是遊戲設計師用描述系統實現的移動模式範例。
例8-8 顯示了我們如何編寫一個指令碼,藉此實現圖8-1 中的行為。
//例8-8:移動模式指令碼
if(creature.state==kPatrol)
begin
move(0,1);
move(0,1);
move(0,1);
move(0,1);
move(0,1);
move(-1,0);
move(-1,0);
move(0,-1);
move(0,-1);
move(0,-1);
move(0,-1);
move(0,-1);
move(0,1);
move(0,1);
end
在此描述範例中,如果 AI 生物處在巡邏狀態中,則使用指定的移動模式。每一步都是從前一位置移動一個單位。要了解移動模式的細節說明,參見 遊戲開發中的人工智慧(三):移動模式。
描述口語互動
智慧行為可以讓遊戲更富有挑戰性,而口語互動也屬於智慧行為。遊戲軟體 AI 必須檢查一組已知的遊戲引數,並據此來做出相應。
例如,玩家的武裝程度怎樣,就是可以被檢查的引數。然後,我們可以讓敵方 AI 角色評論此種武器,看起來就好像計算機控制的角色,可以知道遊戲里正在發生什麼事情一樣。例8-9 是說明這種指令碼的簡單範例。
//例8-9:口語嘲諷指令碼
if(PlayerArmed == Dagger)
Say("好可愛的小刀");
if(PlayerArmed == Bow)
Say("放下弓就讓你活命");
if(PlayerArmed == Sword)
Say("那把劍正好可以讓我當做收藏品");
if(PlayerArmed == BattleAxe)
Say("你沒有力氣揮動那把戰斧");
圖8-2 是一個假象的遊戲場景,一個巨人正在追逐玩家。就此而言,遊戲軟體 AI 可以使用遊戲狀態下特有的元素,根據當前情況,提供適當的嘲諷語。
例8-10 顯示了遊戲軟體 AI,如何在計算機控制的巨人和玩家控制的人類之間的戰鬥中,找出適當的嘲諷語。
//例8-10:巨人嘲諷語指令碼
if(Creature==Giant) and (player==Human)
begin
if(PlayerArmed == Staff)
Say("你需要更多的棍子吧,小矮人!");
if(PlayerArmed == Sword)
Say("放下你的劍,否則我就打扁你!");
if(PlayerArmed == Dagger)
Say("你的小刀抵不上我的棍子!");
當然,這種描述機制不限於哪些要殺玩家的地方角色。友善的計算機控制角色也能利用相同的技巧。例8-11 說明了指令碼如何協助劇情,並引導玩家的行為。
//例8-11:友善NPC的AI指令碼
if(Creature == FreiendlyWizard)
begin
if(playerHas==RedAmulet)
Say("你找到了紅色護身符,把它拿到石廟,你將得到獎賞");
end
如例8-11 所示,直到護身符被玩家得到,並且玩家面對友善的法師時,有關護身符應該放在何處的重要資訊才會展現出來。
前幾個指令碼範例讓你知道了遊戲軟體 AI 可以在指定情況下做出反應,但是有時候,遊戲角色還需要和玩家做某種型別的口語互動。
在這種場景中,玩家必須以某種機制把文字輸入給遊戲。然後,遊戲引擎再把文字字串送交給描述系統,描述系統再分析文字,並提供適當的響應。圖 8-3 顯示了在實際遊戲裡的畫面。
就圖8-3 所示的情況而言,玩家輸入了“What is your name?”,而描述系統做出響應的文字是“I am Merlin”。例8-12 是用於實現此種做法的基本指令碼。
//例8-12:基本的詢名指令碼
If Ask("What is your name?");
begin
Say("I am Merlin");
end
例8-12 有一個嚴重的缺點。只有當玩家輸入和指令碼中的問題完全一樣的文字時,才能起作用。
檢查每個口語文字字串的另一種做法是建立語言分析器,解析每個句子,確定其到底在問什麼。不過對於多數遊戲來說,可以僅僅搜尋特定的關鍵字,並據此作出響應即可。
如例8-14 所示,指令碼將檢查玩家所輸入的文字中,是否有“what”和“name”兩個關鍵字存在。
//例8-14:關鍵字描述機制
If(Ask("what") and Ask("name"))
begin
Say("I am Merlin");
end
下面將介紹如何根據指定關鍵字檢查玩家輸入的問題。例8-15就是這種做法。
//例8-15:搜尋關鍵字
Boolean FoundKeyword(char inputText[kStringLength],char searchFor[kStringLength])
{
char inStr[kStringLength];
char searchStr[kStringLength];
int i;
for(i=0;i<=strlen(inputText);i++)
{
inStr[i]=inputText[i];
if( ((int)inStr[i]>=65) && ((int)inStr[i]<=90) )
inStr[i]=(char)( (int)inStr[i]+32 );
}
for(i=0;i<=strlen(searchFor);i++)
{
SearchStr[i]=searchFor[i];
if( ((int)seachStr[i]>=65) && ((int)searchStr[i]<=90) )
searchStr[i]=(char)( (int)searchStr[i]+32 );
}
if(strstr(inStr,searchStr)!=NULL)
return true;
return false;
}
例8-15 是遊戲引擎中實際的程式程式碼,當遊戲設計師的指令碼中使用了 Ask( ) 函式時,就會被呼叫。這個函式有兩個引數:inputText(玩家輸入的文字)以及 searchFor(要搜尋的關鍵字)。我們在此函式中做的第一件事,就是把字串都轉換成小寫。一旦有兩個小寫字串,我們可以呼叫 C 函式 strstr()來比較兩個字串。strstr()函式會搜尋 inStr 中首次出現 searchStr 的地方,如果在 inStr 中找不到 searchStr,就會返回 false。
描述事件
本節中,我們要檢視指令碼如何觸發與 AI 角色可能不太有直接關聯的遊戲事件。例如,也許站在特定位置上時,就會觸發一個陷阱,如例8-16 所示。
//例8-16:陷阱事件指令碼
If(PlayerLocation(120,76))
Trigger(kExposionTrap);
If(PlayerLocation(56,16))
Trigger(kPoisonTrap);
如例8-16 所示,描述系統可以把玩家位置和某些預設值比較,如果兩者相等,就觸發陷阱。
描述機制也是增加遊戲氣氛的有效方式,可以把某種情況或物體與特定的聲音效果相連線。例8-17 說明了玩家的位置或遊戲的情況(如遊戲事件),觸發相關音效。
//例8-17:觸發音效指令碼
If(PlayerLocation(kDoorway)) //玩家站在門口,播放門嘎吱響的聲音
PlaySound(kCreakingDoorSnd);
If(PlayerLocation(kDock)) //玩家在甲板走動,啟用海鷗音效
PlaySound(kSeagullSnd);
If(GameTime==kNight) //夜晚,播放蟋蟀聲
PlaySound(kCricketsSnd);
If(GameTime==kDay) //白天,播放鳥兒聲
PlaySound(kBirdsSnd);