AI框架 1. 幾種AI的設計 AI在游戲中很多,但是為什麼大家總是感覺ai編寫起來十分困難,我後來思考了一番,主要原因是使用的方法不當。之前大家編寫ai主要有幾種方案: a. 狀態機 我是不知道誰想出來這個做法的,真是無力吐槽。本來對象身上任何數據都是狀態,這種方法又要把一些狀態定義成一種新的節點 ...
AI框架
1. 幾種AI的設計
AI在游戲中很多,但是為什麼大家總是感覺ai編寫起來十分困難,我後來思考了一番,主要原因是使用的方法不當。之前大家編寫ai主要有幾種方案:
a. 狀態機
我是不知道誰想出來這個做法的,真是無力吐槽。本來對象身上任何數據都是狀態,這種方法又要把一些狀態定義成一種新的節點,對象身上狀態變化會引起節點之間的轉換,執行對應的方法,比如OnEnter OnExit等等。這裡以怪物來舉例,怪物可以分為多種狀態,巡邏,攻擊,追逐,返回。怪物的狀態變化有:
巡邏->追逐 巡邏狀態發現遠處有敵人變追逐狀態
巡邏->攻擊 巡邏發現可以攻擊敵人變攻擊狀態
攻擊->追逐 攻擊狀態發現敵人有段距離於是去追逐
攻擊->返回 攻擊狀態發現距離敵人過遠變返回狀態
追逐->返回 追逐狀態發現距離敵人過遠變返回狀態
太多狀態轉換了,這裡有沒有漏掉我已經難以發現了。一旦節點更多,任何兩個節點都可能需要連接,將成為超級複雜的網狀結構,複雜度是N的平方級,維護起來十分困難。為瞭解決網狀結構變複雜的問題於是又升級為分層狀態機等等。當然各種打補丁的方法還是沒能解決本質的問題。用不好狀態機不是你們的問題,是狀態機的問題。
b. 行為樹
可能大家都覺得狀態機解決複雜ai實在太困難了,於是有人想出了行為樹來做ai。行為樹的ai是響應式ai,這棵樹從上往下(或者從左往右執行,這裡以從上往下舉例)實際上是把action節點排了個優先順序,上面的action最先判斷是否滿足條件,滿足則執行。這裡就不詳細講了。行為樹的複雜度是N,比狀態機大大簡化了,但是仍然存在不少缺陷,ai太複雜的時候,樹會變得非常大,而且難以重構。比如我們自己項目,要做一個跟人差不多的機器人ai,自動做任務,打怪,玩游戲中的系統,跟人聊天,甚至攻擊別人。想象一下,這顆樹將變得多複雜!行為樹的另外一個缺陷是某些action節點是個持久的過程,也就是說是個協程,行為樹管理起協程起來不太好處理,比如上面的例子,需要移動到目標身邊,這個移動究竟是做成協程呢,還是每幀move呢?這是個難題,怎麼做都不舒服。
2. 我的做法
ai是什麼呢?很簡單啊,ai就是不停的根據當前的狀態,執行相應的行為。記住這兩句話,很重要,這就是ai的本質!這兩句話分成兩部分,一是狀態判斷,二是執行行為。狀態判斷好理解,行為是啥?以上面狀態機的怪物舉例子,怪物的行為就是 巡邏,攻擊敵人,返回巡邏點。比如:
巡邏 (當怪物在巡邏範圍內,周圍沒有敵人,選擇下一個巡邏點,移動)
攻擊敵人 (當怪物發現警戒範圍內有敵人,如果攻擊距離夠就攻擊,不夠就移動過去攻擊)
返回 (當怪物發現離出生點超過一定距離,加上無敵buff,往出生點移動,到了出生點,刪除無敵buff)
跟狀態機不一樣的是,這3個狀態的變化完全不關心上一個狀態是啥,只關心當前的條件是否滿足,滿足就執行行為。行為可能能瞬間執行,也可能是一段持續的過程,比如巡邏,選下一個巡邏點移動過去,走到了再選一個點,不停的迴圈。比如攻擊敵人,可能需要移動到目標去攻擊。
怎麼設計這個ai框架呢?到這裡就十分簡單了,抽象出ai節點,每個節點包含條件判斷,跟執行行為。行為方法應該是一個協程
public class AINode { public virtual bool Check(Unit unit) // 檢測條件是否滿足 { } public virtual ETTask Run(Unit unit) { } }
進一步思考,假如怪物在巡邏過程中,發現敵人,那麼怪物應該要打斷當前的巡邏,轉而去執行攻擊敵人的行為。因此我們行為應該需要支持被打斷,也就是說行為協程應該支持取消,這點特別需要註意,行為Run方法中任何協程都要支持取消操作!
public class AINode { public virtual bool Check(Unit unit) { } public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken) { } }
實現三個ai節點 XunLuoNode(巡邏) GongjiNode(攻擊) FanHuiNode(返回)
public class XunLuoNode: AINode { public virtual bool Check(Unit unit) { if (不在巡邏範圍) { return false; } if (周圍有敵人) { return false; } return true; } public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken) { while (true) { Vector3 nextPoint = FindNextPoint(); bool ret = await MoveToAsync(nextPoint, cancelToken); // 移動到目標點, 返回false表示協程取消 if (!ret) { return; } // 停留兩秒, 註意這裡要能取消,任何協程都要能取消 bool ret = await TimeComponent.Instance.Wait(2000, cancelToken); if (!ret) { return; } } } }
同理可以實現另外兩個節點。光設計出節點還不行,還需要把各個節點串起來,這樣ai才能轉動
AINode[] aiNodes = {xunLuoNode, gongjiNode, fanHuiNode}; AINode current; ETCancelToken cancelToken; while(true) { // 每秒中需要重新判斷是否滿足新的行為了,這個時間可以自己定 await TimeComponent.Instance.Wait(1000); AINode next; foreach(var node in aiNodes) { if (node.Check()) { next = node; break; } } if (next == null) { continue; } // 如果下一個節點跟當前執行的節點一樣,那麼就不執行 if (next == current) { continue; } // 停止當前協程 cancelToken.Cancel(); // 執行下一個協程 cancelToken = new ETCancelToken(); next.Run(unit, cancelToken).Coroutine(); }
這段代碼十分簡單,意思就是每秒鐘遍歷節點,直到找到一個滿足條件的節點就執行,等下一秒再判斷,執行下一個節點之前,先打斷當前執行的協程。 幾個使用誤區:
- 行為中如果有協程必須能夠取消,並且傳入cancelToken,否則會出大事,因為怪物一旦滿足執行下個節點,需要取消當前協程。
- 跟行為樹與狀態機不同,節點的作用只是一塊邏輯,節點並不需要共用。共用的是協程方法,比如MoveToAsync,怪物巡邏節點可以使用,怪物攻擊敵人節點中追擊敵人也可以使用。
- 節點可以做的非常龐大,比如自動做任務節點,移動到npc,接任務,根據任務的子任務做子任務,比如移動到怪點打怪,移動到採集物去採集等等,做完所有子任務,移動到交任務npc交任務。所有的一切都是寫在一個while迴圈中,利用協程串起來。
思考一個大問題,怎麼設計一個壓測機器人呢?壓測機器人需要做到什麼?自動做任務,自動玩各種系統,自動攻擊敵人,會反擊,會找人聊天等等。把上面說的每一條做成一個ai節點即可。兄弟們,AI簡不簡單?
ET開源地址地址:egametang/ET: Unity3D Client And C# Server Framework (github.com) qq群:474643097