又是閑得蛋疼的幾周呀~~~~~這段時間我理論和實踐了所謂的Behavior Tree(行為樹).
行為樹的介紹可以看這里:
http://www.aisharing.com/archives/tag/%E8%A1%8C%E4%B8%BA%E6%A0%91
經過這幾周的努力,我目前已經在自己的MiniCraft中增加了行為樹模塊,包括可視化工具.先簡要闡述下我的實現方法,然后我們再來實際構建一個行為樹,讓它跑起來.
我實現了三種行為樹節點:序列(Sequence)節點,條件(Condition)節點和行為(Action)節點.
序列節點是從左到右依序執行,直到某個子節點執行成功.
條件節點只能有一個子節點,若條件滿足則執行子節點.相當有趣的是,就我的領悟,行為樹的特點是很大程度上是數據驅動的(黑板,條件節點條件定義),這里的條件是用戶自己定義的表達式字符串,比如 UnitNum == 3,這就涉及到了語句解析.嘗試實現過一遍之后,我感覺就像在搞簡化的編譯器一樣!其實我這里最關鍵這個lexer是用CE3的行為樹的條件解析器代碼,它沒有支持算術判定和變量,我用很丑和低效的實現加上了.只能說是能用起來而已.
行為節點必須是葉子節點,它代表了一個最終被執行的行為,比如向敵人基地移動.行為樹的葉子節點必須是行為節點,這就保證了每次遍歷樹后必會產生一個待執行的行為.每個行為可是代碼定義的一個類或一個腳本函數(目前我只支持了代碼類).
很重要的一個概念是"黑板(Blackboard)".它是整個行為樹的數據倉庫,能輔助條件節點的判定.我們可直接把它看成Ogre::NameValueList,即一個鍵值表,怎么定義數據項完全由用戶決定,比如定義一個整數類型參數UnitNum與上面的UnitNum == 3相對應.關鍵就是如何更新黑板的數據,直接在代碼里肯定不行,行為樹系統就是要完全脫離代碼修改,使得策划人員能快速進行工作流.那當然是用腳本了,於是我設計每個行為樹可定義一個黑板更新lua函數,用戶在其中調用客戶端腳本函數獲取所需信息,然后用來更新黑板.當然,這樣就涉及到了客戶端需要提供一個相當完善的導出函數功能集,不過我想這一點應該是順風順水的.這樣,通過黑板,條件判定,腳本和可視化工具的配合,就能讓策划自己去擺弄了.
核心的東西就是這些,現在我以我剛完成的行為樹系統為依托,來實際演練一個比較簡單的Behavior制作流程------星際2的Scv采礦.之前我已經實現了FSM(有限狀態機)的版本,現在換行為樹玩玩了 :D
先比比划划在紙上分析出整個邏輯結構,再用編輯器產出(其實我想說的是直接在紙上分析出來,然后寫xml就行了,但是通常這是給策划等非技術人員用的啊,必須得提供編輯器囧).
預想的樹結構應該是這個樣子:
簡單分析下:
Root是空條件節點必執行,往下先判定Scv當前是否已載有資源(IsCarryingRes),滿足則繼續判定它是否回到了基地(IsNearBase),是則返還資源(ReturnRes),否則移動回基地(MoveToBase).
這是左邊第一條支路,若未成功執行則來到中間這條,判斷Scv是否處在辛勤的采集狀態(IsGathering),滿足則繼續判定已采集的時間是否達到了3秒(fGatheringTime>=3),是則讓其獲取到了資源(GetRes),否則繼續采集行為吧(Gathering)
若Scv又沒載有資源,又沒在采集,則判定第三條支路,其是否正處於采集點(IsNearRes),是則開始采集行為(Gathering)
最后,以上支路都沒滿足,那么Scv必定處於向資源點移動行為中(MoveToRes).
假設這個花1小時分析清后,那么用工具產出只要10分鍾........如下圖:
產出的xml如下:
<?xml version='1.0' encoding='utf-8' ?> <Root> <BehaviorTemplate name="Scv" race="Terran"> <BehaviorTree> <SequenceNode> <ConditionNode expression="IsCarryingRes==true"> <SequenceNode> <ConditionNode expression="IsNearBase==true"> <ActionNode behavior="ReturnRes"/> </ConditionNode> <ActionNode behavior="MoveToBase"/> </SequenceNode> </ConditionNode> <ConditionNode expression="IsGathering==true"> <SequenceNode> <ConditionNode expression="fGatheringTime greaterequal 3.0"> <ActionNode behavior="RetriveRes"/> </ConditionNode> <ActionNode behavior="GatherRes"/> </SequenceNode> </ConditionNode> <ConditionNode expression="IsNearRes==true"> <ActionNode behavior="GatherRes"/> </ConditionNode> <ActionNode behavior="MoveToRes"/> </SequenceNode> </BehaviorTree> <BlackBoard> <Variable name="IsCarryingRes" value="false" type="bool"/> <Variable name="IsGathering" value="false" type="bool"/> <Variable name="IsNearRes" value="false" type="bool"/> <Variable name="IsNearBase" value="false" type="bool"/> <Variable name="fGatheringTime" value="0" type="float"/> </BlackBoard> <Script filename="ScvBlackboard.lua" entry="BBUpdate_Scv"/> </BehaviorTemplate> </Root>
最后是我們的Scv的黑板更新腳本函數:
1 --與代碼相對應 2 eHarvestStage_ToRes = 0 3 eHarvestStage_NearRes = 1 4 eHarvestStage_Gather = 2 5 eHarvestStage_Return = 3 6 eHarvestStage_NearBase = 4 7 eHarvestStage_None = 5 8 9 -------------------------------- 10 ---Scv blackboard 11 --------------------------------- 12 function BBUpdate_Scv(unitID) 13 obj = UnitTable[unitID] 14 curStage = obj:GetHarvestStage() 15 16 isGathering = false 17 isCarryRes = false 18 isNearBase = false 19 isNearRes = false 20 21 if curStage == eHarvestStage_NearRes then 22 isNearRes = true 23 elseif curStage == eHarvestStage_Gather then 24 isGathering = true 25 elseif curStage == eHarvestStage_Return then 26 isCarryRes = true 27 elseif curStage == eHarvestStage_NearBase then 28 isCarryRes = true 29 isNearBase = true 30 end 31 32 obj:SetBlackboardParamBool("IsCarryingRes", isCarryRes) 33 obj:SetBlackboardParamBool("IsGathering", isGathering) 34 obj:SetBlackboardParamBool("IsNearRes", isNearRes) 35 obj:SetBlackboardParamBool("IsNearBase", isNearBase) 36 37 fTime = obj:GetGatheringTime() 38 obj:SetBlackboardParamFloat("fGatheringTime", fTime) 39 end
Ok了,只是所有的行為我目前都是在代碼中完成的,還是需要提供腳本支持才好,行為類像下面這個樣子:
1 ///向可采集資源移動 2 class aiBehaviorMoveToRes : public Kratos::aiBehavior 3 { 4 public: 5 virtual void Execute(Ogre::Any& owner); 6 virtual void Update(Ogre::Any& owner, float dt); 7 virtual void Exit(Ogre::Any& owner); 8 };
執行游戲后發現結果跟FSM是一致的,如下圖: