Behavior Tree实践
又是闲得蛋疼的几周呀~~~~~这段时间我理论和实践了所谓的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是一致的,如下图: