概念
行为树一般在做游戏AI的时候经常用到,Unity3d就有一款专门的行为树插件,Unreal4里面也在引擎层支持行为树编辑。本人之前是做Unity3d手游的,对这些有所涉猎,最近在用go语言写后台,我们刚开发了几个服务器,需要在后台编写一个程序监控服务的可用性,如果不可用可用报警之类的,然后我就想到了用行为树来实现这个功能。
大家可能一直都是用插件的那种可视化工具配置行为树,顶多就是写写树的执行节点,这里我为大家写一整套树的实现。
对于没有接触过行为树概念的人来讲可能有些困难,这里我简单做一些介绍,如果还是有疑惑,可用自行脑补。
这就是一个行为树了,有根节点,有子节点,而且子节点个数是任意个,Patrol,Attack,Retreat,可以看成是一个士兵的行为,如果有更多的行为可以自行加进去,每次执行AI时,系统都会从根节点遍历整颗树,父节点执行子节点,根据子节点返回的结果判断接下来执行什么。
常见的基础类型节点有以下几类:
1)顺序节点(Sequence):属于组合节点,顺序执行子节点,只要碰到一个子节点返回false,则停止继续执行,并返回false,否则返回true,类似于程序中的逻辑与。
2)选择节点(Selector):属于组合节点,顺序执行子节点,只要碰到一个子节点返回true,则停止继续执行,并返回true,否则返回false,类似于程序中的逻辑或。
3)平行节点(Parallel Node):提供了平行的概念,无论子节点返回值是什么都会遍历所有子节点。所以不需要像Selector/Sequence那样预判哪个Child Node应摆前,哪个应摆后。Parallel Node增加方便性的同时,也增加实现和维护复杂度。
4)条件节点(Condition):属于叶子节点,判断条件是否成立。
5)执行节点(Action):属于叶子节点,执行动作,一般返回true。
实例
接下来我就来讲解我是怎么用golang来实现一个行为树基础框架的,代码开源,文章末尾会贴出github地址,欢迎来沟通
节点描述
//ResultType result enums 节点返回结果枚举
type ResultType uint16
const (
B_FALSE ResultType = iota
B_TRUE
B_RUNING
)
//BaseNode basenode 节点interface
type BaseNode interface { IsInit() bool SetParent(parent BaseNode) SetIndex(index int) Tostring() WhoAmI() string SetTree(t *Tree) OnInstall() OnUnstall() OnEnter() OnExit() ExcuserNode(child BaseNode) SendParentResult(exitNode BaseNode, result ResultType) OnChildrenFinish(result ResultType, childIndex int, owner string) }
//AiNode node describe 节点数据描述
type AiNode struct { ChildCount int IsAlInit bool Parent BaseNode nodeList []BaseNode curNode BaseNode IdxInParent int Owner string }
sequence顺序节点实现
//AddNode add node 变长参数加入节点
func (s *SequenceAINode) AddNode(arg ...BaseNode) {
for i, v := range arg {
v.SetIndex(i)
v.SetParent(s)
s.nodeList = append(s.nodeList, v)
}
s.ChildCount = len(arg)
}
//OnEnter enter sequence进入
func (s *SequenceAINode) OnEnter() {
if len(s.nodeList) > 0 {
s.curNode = s.nodeList[0]
s.ExcuserNode(s.curNode)
} else {
s.OnExit()
s.SendParentResult(s, B_TRUE)
}
}
//OnChildrenFinish child is done sequence节点结束
func (s *SequenceAINode) OnChildrenFinish(result ResultType, childrenIndex int, owner string) {
fmt.Printf("%s childNode return %d\n", owner, result)
if result == B_FALSE {
s.SendParentResult(s, B_FALSE)
return
}
fmt.Printf("childrenIndex:%d childCount:%d\n", childrenIndex, len(s.nodeList))
if childrenIndex .nodeList)-1 {
s.curNode = s.nodeList[childrenIndex+1]
s.ExcuserNode(s.curNode)
} else {
s.SendParentResult(s, B_TRUE)
}
}
behaviour 具体行为配置
//Init init
func (b *Behaviourer) Init() {
b.executor = baseai.NewTree()
root := baseai.NewRootNode()
seq := baseai.NewSequence()
action1 := NewAction1()
action2 := NewAction2()
seq.AddNode(action1, action2)
root.AddNode(seq)
b.executor.SetRoot(*root)
}
//Run run
func (b *Behaviourer) Run() {
b.executor.Run()
}
这边我写了2个执行节点Action1和Action2,执行结果如下
Action1 SetParent
Action2 SetParent
Rootnode AddNode
tree excuser
Rootnode OnInstall
Rootnode OnEnter
excuse SequenceAINode node
excuse action1 node
Action1 OnInstall
Action1 OnEnter
Action1 OnExit
action1 childNode return 1
childrenIndex:0 childCount:2
excuse action2 node
Action2 OnInstall
Action2 OnEnter
Action2 OnExit
action2 childNode return 0
SequenceAINode root childNode return 0
node:SequenceAINode run over!
这里看到Action1先执行完了再执行Action2,按顺序执行,如果Action1执行失败的话就不会执行Action2,这里需要说明一哈,执行节点就是具体你需要去做的行为,行为树控制你的行为逻辑。好比文章开头的行为树,执行节点可以是去巡逻,攻击等,先巡逻,然后发现敌人,最后攻击。用行为树来做这样的事情是最合适的,而且维护拓展也比较方便。
这边我只写了顺序节点的实现,暂时只用到了这个,有兴趣的同学可以写其他基础节点
源码地址:https://github.com/fdgggy/BehaviourTree_Golang