如何建立一個完整的游戲AI


http://blog.friskit.me/2012/04/how-to-build-a-perfect-game-ai/

人工智能(Artificial Intelligence)在游戲中使用已經很多年了,並且到現在越來越完善。如果你不在你的游戲中加入完善的游戲智能,那么別人就認為你的游戲缺少可玩性。
在游戲中,AI並不一定要包括神經網絡,學習系統和復雜的數學結構,游戲AI只是游戲中一個重要部分,它是活動的,並不是科學性質的。
我認為如何建立一個游戲AI,最主要的就是要明白你想在游戲中實現什么效果,就是你想讓玩家看見什么;如果游戲中什么也沒有發生,那你的游戲AI什么都沒有做。
在本篇中,我們將討論一個實時策略游戲(RTS)中游戲AI,不管怎樣這些理論都可以很好地移植到其它的系統當中,所有的代碼全部用c語言寫成。

狀態機

有限狀態機

有限狀態機(FSM)是一個只有有限的幾個狀態的系統。在一個實際的例子當中,狀態的觸發是通過一個擁有開或關狀態的開關,或通過一個鬧鍾來調用時間決定的。通過有限狀態機,我們可以在游戲中定義一些事件,然后由玩家在游戲中游動時,通過觸發來實現某些事件。

有限狀態機是游戲中最常用的游戲AI。下面,我們將講解如何在游戲中使用有限狀態機。

使用有限狀態機

在游戲中,我們最常用的就是用有限狀態機來模擬人的某些智能行為。比如,當一個NPC受到攻擊時,它應該怎么辦,當發現敵人時,它應該實行哪些行為,當敵多我寡時,應該實行哪些行為,等等。都可以用有限狀態機來解決。
你可以在游戲中,將你的游戲AI設計的完美無疵,它可以萬全模擬一個人的思維,能夠自我思考,自我學習,但你要記住一點,在游戲中,我們應盡量將AI簡單化,不要太科學化。
並不要以為,我們在這里反對神經網絡,遺傳算法等等,你要在游戲中使用哪種算法,完全由你自己根據你在游戲中要實現一個什么效果,完成一個什么目標來決定。相反,我們在這里最主要就是講解如何在游戲中使用有限狀態機。

游戲狀態機

要在游戲中實現一些動作行為,你必須考慮很多方面,比如你就不能從你自身的角度來考慮問題,你就得從玩家的角度來考慮,玩家他到底要在游戲中干什么?要想達到玩家的想法,你就不得不在游戲中反復測試,以便於達到玩家想要的效果。
在一個實際的游戲當中,一般存在兩種狀態機:
第一種狀態機主要就是完成游戲界面的轉換,比如,玩家在游戲中暫停,應該顯示什么界面,游戲中哪些UI應該讓玩家可見,哪些又應該隱藏。
第二個狀態機主要就是改變當前運行時的環境,比如當前玩家處於哪個游戲地圖,關卡中NPC的出現,玩家任務完成的狀態:成功或失敗。還有就是我們可能在游戲中要引導玩家的一些參數或變量。
你可能需要一個描述NPC的系統,這個系統主要就是用於判斷如果玩家點擊NPC或者向NPC開火,NPC應該怎么辦,我們完全可以用下面的結構來表示:

struct GameLevelState{
	int alert; //NPC的當前狀態
	struct Positionshot; //玩家向NPC開火的位置
	int shotTime; //子彈發射出去后的游戲循環
	int hostage; //誰需要幫助
	int explosives; //子彈是否爆炸
	int tank; //自身是否受到破壞
	int dialogue; //對話參數
	int compete; //任務是否完成
};

擴展性

保證你的AI擁有良好的擴展性是重要的。在編程的時候,你要能夠使你的AI最重要的部分能夠很容易的擴展。因為你要明白游戲AI的設計是一個迭代的過程,你需要不斷地去測試它,完善它。

群體動作

在游戲中,是由一個玩家來控制群體,什么是群體動作呢?比如,在中,玩家選中一群士兵,然后然它們朝一個目的地前進,如果沒有良好的群體控制,那么這些士兵可能就會越走越散。這樣就完全不符合玩家的思想。

解剖101

下面,我們來詳細地看一下一個角色的數據結構:

struct Character{
	struct Positionpos; //玩家所在的地圖位置
	int screenX,screenY; //玩家所在的屏幕位置
	int animDir, animaction, animNum; //動畫信息,角色動作和動畫幀數
	int rank; //軍銜
	int health; //生命值
	int num; //在當前群體中有多少人
	int group; //部隊編號
	int style; //部隊的性質(步兵,騎兵等)
	struct AnimationObject animObj; //復雜動畫的動畫對象
};

現在,我們來詳細講解每一個參數:
pos參數用於決定部隊在屏幕和游戲世界中的位置,這樣是為了便於我們在屏幕的相應位置播放精靈動畫,顯示精靈信息等等。
AnimDir,animaction,animnum是用於決定我們當前應該在屏幕上顯示哪段精靈動畫。
Rank ,health 就不用說了。
Num 參數用於決定該角色在部隊中的ID號。
Group參數用於決定該角色所在部隊在所有部隊中的編號。
Style,animobj參數用於決定部隊的圖像在屏幕上的顯示。

建立過去基本

一旦你利用前面的方法建立了一個初始化部隊的函數,那么現在就需要你對這些部隊賦予生命。
這時候,你就需要思考你想讓你的部隊擁有什么樣的動作,有什么樣的反應。你需要思考,你的部隊是否感情用事?是否像一個瘋子?是否需要被凍結?
要做到這些,你就必須首先思考當人遇到某某事情時,他應該怎樣做,然后再把你的思考變成代碼,觀看其效果,不對就再修改。

 

群組

群組或者非群組

如果你是制作第一人稱射擊游戲,你就不用考慮群組的問題了,因為就只有你一個。但當你想建立一個RTS類形的游戲,你想同時控制很多人的動作,那你就不得不考慮下面的問題:
我是否需要我的部隊按照一種比較協調的方式行進?
如果回答是“是”,那么恭喜你,群組適合你。

群組的優點

1. 部隊能夠按照一個主要的移動信息列表前進。這樣做的好處就是群組中的任何一個單元當受到其他信息,比如目標的改變等,其它成員還是可以按照先定的移動信息前進。
2. 多個成員自我調節行為。 比如,我們控制一個部隊包圍一個建築物,部隊中的成員能夠相互協作完成對建築物的包圍。
3. 群組能夠自動保持他們的結構,這樣,當一個部隊遇到一些不明情況時,他能夠自適應的告訴部隊中的每個成員該干什么。比如,行進當中要注意保持對形,先頭部隊首先遭遇敵人,應奮起阻擊。
4. 依賴於群組的構成,你可以直接控制群組來控制群組中的其他成員,這樣就簡化了找路徑等問題的麻煩程度。

大圖片

如果你想在游戲AI中判斷你所有部隊的動作,你就不得不把這些部隊的信息全部群組起來。一個好注意就是建立一個地方,用於共享所有部隊的信息。
根據我的經練,這個部分應該分兩步完成:第一部分就是將群組中的每個成員應該做什么事存儲在每個成員自己的數據結構中,而其他需要組織和共享的信息,比如移動,行動等動作,應存儲在群組數據結構中。
這意味着部隊不擁有移動的信息,他們總是在群組的權利范圍之下做出決策。如果硬要說每個部隊都是獨立的,那他們也只是在群組下獨立。

許多中的一個

當我們為了適應系統的變化時,尋找一個群組去行動,那系統將建立一個單獨的部分,並且最重要的是保持每個部隊的行動信息。這樣做的目的是便於我們打亂我們的結構然后完成一些特殊的任務。這樣的數據結構如下:

struct GroupUnit{
	int unitNum; //角色ID
	struct Unit *unit; //部隊角色數據
	struct Positionwaypoint[50]; //所有部隊的路徑
	int action[50]; //部隊在路徑上應該的動作
	int stepX,stepY; //各別部隊的步驟
	int run,walk,sneak,fire,hurt,sprint,crawl; //行動
	int target; //所有部隊的目標
	struct Position targetPos; //目標位置
};

下面,我們來具體描述這些參數:

unitNum是部隊在群組中的ID。如果群組容納的最大部隊數是10,那么最小的部隊編號是0,最大的是9。
Unit 是一個指向部隊角色數據的指針,它存儲了包括角色當前的位置,生命值和其它相關信息。
Waypoint 包含了一個部隊可以去的所有地方。所有行動和位置信息是包含在GroupUnit結構中,如果群組不是在一個結構中,那么部隊中的所有成員將按照它們自己的方式移動。
Action數組包含了移動到目標的所有動作。這允許你建立一個詳細的動作鏈,以便於玩家控制部隊在森林里潛行,然后匍匐着靠近目標。
StepX ,stepY用於存儲簡單的速度信息;因為每個兵種,它們的移動速度都是不一樣的,總不可能步兵的移動速度比火箭飛行兵快吧。
Target ,targetpos參數用於存儲當我們選中了一個敵人部隊時,敵人部隊的編號和位置。關於敵人的其它信息,我們可以通過每次的游戲循環來找出。

群體心理

我們還需要一個中央集權的群組來管理我們所有的群組,下面,我們來看一下一個簡單的結構。

struct Group{
	int numUnits; //群組中的部隊數
	struct GroupUnit unit[4]; //部隊信息
	int formation; //部隊和群組的結構信息
	struct Position destPos; //目標
	int destPX,destPY; //目標屏幕坐標
	struct Postion wayX[50]; //群組的所有可去的目標點
	float formstepX,formstepY; //群組移動的速度
	int formed; //如果這個值為真,那么部隊獨立行動,否則按照群組的規則移動
	int action,plan; //群組行動和計划
	int run,walk ,sneak,sprint,crawl,sniper; //具體動作
	struct Position spotPos; //攻擊地點
	int strategyMode; //群組的戰越模式
	int orders[5]; //群組的順序
	int goals[5]; //群組的目標
	int leader; //群組的領導者
	struct SentryInfo sentry; //哨兵列表
	struct AIState aiState; //Ai狀態
};

numUnits是指在群組中有多少部隊,部隊數組存儲在GroupUnit結構中,在本例中,我們存儲的最大數量是4.
Formation標志決定了群組是采用了什么隊形,它們可以根據自身的狀況分別采用柱,楔和三角等隊形。
DestPos,destPX,destPY描述了群組在趕往目的地中途的什么地方。Waypoints,steps以相同的方式工作,它們都是描述了部隊行進的速度。在這里,並不需要部隊中每個成員來設置自己的速度,這個事完全由群組決定。
Formed 是最為重要的參數之一,它決定了部隊是否按照對形行進或單獨行進。如果那個群組被設置成按對形行進,那么群組中所有的部隊就必須按照群組設置的對形行進,但是當遇到某些特殊的原因,比如遭受攻擊或對形遭到了破壞,那么部隊就按照他們自己的思路前進。
Actions參數就是表示部隊在到達目的地中間的動作,比如跑,走,潛行等等,完全由玩家決定。
Strategymode 決定了當部隊遇到敵人后應該怎么辦,部隊是反抗還是逃跑?
Orders,goals數組是為了便於群組之間的消息傳輸。
Sentry , aiState存儲了哨兵信息和更為詳細的AI信息,這些信息是為了更詳細的模型匹配。

把它放在一起

到現在,關於我們的群組,我們已經有了一些結構。然后我們應該做什么呢?下一步就是找出我們在游戲中如何用編程來實現這些信息。
值得注意一點的就是,盡量將你的AI程序模塊化,這樣做的好處就是便於以后更好地擴充它們。詳細一點就是說,你在編寫函數時,盡量做到一個函數只做一件事情。

關鍵部分

在你准備寫你的游戲之前,請不斷的學習一些基本的東西,如果你的游戲需要一些功能函數,那么就為這些功能編寫相應的函數。
不要學習某些高手用一個函數完成所有的功能,可能一個函數對他很管用,但對你就不那么行的通。
你要不斷的創新,不斷的挑戰傳統,不要看一些看似經典的程序你需要自己創造。如果你寫的東西,它能正常工作,那么用它。在游戲編程中有一句經典的話是這樣說的:“如果你覺得它是對的,它就是對的”。
不要被其它的思想絆住了腳,因為我們相信你是最強的。

其實,AI的意思就是如何讓決策變得更聰明,在我們這篇文章中,就是要讓玩家感覺游戲中的人物像真的一樣。
在一個RTS(實時策略游戲)游戲中,我們所謂的動作包括移動,巡邏,避開障礙物,打擊敵人和追趕它們。讓我們來看一下每個動作的詳細內容。

移動

移動,最簡單的一種形式就是,在某一段時間之內從一個點到達另一個點。這個很容易實現,你能通過找一個距離向量,然后乘以這個部隊移動的速度和所用的時間,就可以得到移動的位置。
因為我們的游戲是通過鼠標來驅動的,所以,我們並不能期望用戶通過游戲桿之內的東西來驅動游戲人物前進時繞開障礙物,首先,你要明白一點,我們這里說的並不是第一人稱射擊類游戲。
我們應該怎樣做呢?比如,玩家在一棵大樹旁點擊了鼠標,我們首先必須建立一個動作隊列保存這個角色行動的路徑,但是當角色到達大樹時,他不可能穿牆而過吧。這個時候,我們就必須為角色添加一個動作,以便於角色繞過大樹。

巡邏

巡邏是一個特別的移動,因為它包含了一系列預設的坐標。在移動中,一個部隊它走到一個目標以后,它就不會走了,但巡邏不一樣,當部隊完成一個目標以后,他就會從自身的巡邏列表中抽出下一個坐標,然后又朝下一個坐標前進。
在我們這個實例當中,盡量不要讓部隊一直站立或等待,因為我們這個游戲是RTS游戲,如果一直站立或等待就不能突出游戲的戰爭氣氛。

避開障礙物

避開障礙物的算法首先要看你的地圖是如何工作的,還有就是你的部隊在移動時應該讓他們如何相互交互。在本例中,我們使用的是一個擁有很多小的建築物和物體的室外環境。在這個游戲中,你不能進入建築物,這些建築物都是用一個凸出的擁有4個頂點的多邊形表示。

在下面這個例子當中,部隊和目標並不是成直線時。首先,我們先移動到和目標最近的一個坐標,這個坐標盡量和目標呈直角。然后再從這個點移動到目標。這樣做的好處就是留下了很大的緩沖空間讓我們繞過障礙物。
說白了,障礙物避開算法是由你將遇到的障礙物決定的。如果你想了解一些更好的路徑尋找算法,我建議你看一下A*算法。A*算法是一個非常流行的最短路徑尋找算法。

打擊敵人

和其它部隊作戰通常依賴於你想你的玩家在游戲中能干什么。你可能想你的玩家的部隊在見到敵人時能夠自動開火,以便於玩家能夠將注意力放在整個戰場上。你可能還想你的部隊能夠監視周圍的環境,如果出現敵人就馬上開火。
比如,一個角色站在一個坐標上,可以將它的四周分為8個部分,這8個部分分別代表了角色的8個視域,當然你也可以設置這些視域的距離,也就是角色所能看到的距離。然后就循環監測這些視域中是否有敵人。當然,也有一種特殊情況,比如,敵人和障礙物同時出現在你的視域中,障礙物在前,敵人在后,你總不可能也看見敵人了吧!關於這個問題,我將在另一篇文章中講解。

追趕

當一個敵人發現了一個獵物,也就是它自身的視域中出現了一個角色,這時候,它就要判斷,這個角色是自己人嗎?如果是,就不干什么。如果是獵物,就要檢測它是否在視域內,如果是,就開槍射擊。如果不是在視域內呢?那么就需要追蹤它。
那將如何實現呢?首先,你需要保存獵物最后的一個坐標,然后將這個坐標設置成你的第一個移動目標,到達目標后,你就需要檢測獵物是否還在視域范圍內,如果在,就開槍射擊。如果不在,就按照獵物的方向隨機移動一個距離,然后繼續檢測。
在本篇的例子當中,我們假設角色能夠在第一次移動過后找到獵物。但如果他移動后沒有發現獵物,那么我們就將他的狀態設置成巡邏狀態,然后朝我們第一次得到的獵物的方向移動一個隨機的距離。

總結

當你看懂了這篇文章,並且在程序中將它實現以后,你就會發現,其實游戲AI也沒那么神秘,不是挺簡單的嗎?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM