前言
我最近学习使用C#脚本实现Unity行为树,并使用行为树实现了对“空洞骑士”中,“假骑士”的AI行为逻辑的简单实现。本文主要记录了在这个过程中的一些要点。
行为树的原理及实现教程来自这位大佬的博客:
游戏AI行为决策——Behavior Tree(行为树)
一、运作逻辑
行为树的运作逻辑在大佬的博客中有详细说明,这里为了方便我自己查阅,就简单描述一下。
与状态机(FSM)不同,状态机的原理是停留在某一状态,重复执行该状态的逻辑,直到达成条件转换为别的状态;而行为树是不断从根节点向下搜索(根节点驱动),找到达成条件的节点执行,执行完成后重新从跟节点开始,一直重复这个过程。
行为树顾名思义是一个树状结构,它具有树状结构的特点,开发者可以灵活地进行组装,实现节点的重复利用,避免写重复的代码,提高了开发效率。其缺点可能就是每次都要从根节点重新遍历,性能开销和复杂性略大于有限状态机。
行为树的节点类型包含以下几种:
- 组合节点(Composite),指有多个子节点的特殊节点,具体包括:
- 顺序器(Sequence)
- 选择器(Selector)
- 并行器(Parallel)
- 过滤器(Filter)
- 主动选择器(ActiveSelector)
- 监视器(Monitor)
- 修饰节点(Decorator),指仅有一个子节点的特殊节点,具体包括:
- 取反器(Inverter)
- 重复执行器(Repeat)
- 动作节点,指可以自定义的节点,比如「攻击」、「巡视」之类的
树形结构的实现则是采用链表和栈,利用链表按顺序记录节点的所有子节点,利用栈暂时记录根节点便于调用。
具体的实现代码在大佬的博客中有详细代码,这里不多赘述,为了方便理解这里贴出我在学习后整理出的类图
(最后所有的节点都会聚合到BehaviorTreeBuilder中统一调用,类图中为了美观没有一一连线)
二、行为树的应用
行为树一般用于实现AI行为逻辑,我使用了我半年前用有限状态机制作的一个空洞骑士假骑士boss战的一个项目进行行为树应用练习,将假骑士的有限状态机更改为行为树
1. 设计行为树
在没有可视化的情况下,构建一个行为树还是比较复杂的,容易混乱,建议先像我这样将树形结构画出来,对照着图一步步构建。
我计划使用顺序器(Sequence)和选择器(Selector)组合以实现这个行为树。根据行为树和组合节点的特点:
选择节点会依次遍历检查它的子树,如果有一个子树成功执行就会返回成功并停止遍历,执行完成后重新从头开始
顺序节点会依次遍历检查它的子树,如果有一个节点执行失败就会返回失败并停止遍历,然后重新从头开始
我们可以将执行节点的条件(上图中连接线上的语句)单独作为节点放在行为节点前。按照这个思路,我将行为树的模拟图进行细化
蓝色的节点为条件节点,我又加了攻击和追击的cd判定,防止怪物不停的追着玩家或者不停的攻击。
利用选择节点从左到右依次检查的特性,我将待机节点放在最后,这样就不用为待机加条件节点了,前面的的节点如果都没有达成运行条件,就会运行待机。
2.节点实现
对于行为节点的实现,我相信大家各有各的手段,我就不展示我杂乱且业余的的代码了。这里主要说以下cd判定节点的实现。cd计时器我使用了协程,当可执行标记为true时进入协程并返回success,协程中将标记置为false,并在cd时间(我设置为5秒)后将标记置为true。
代码很简单,但是问题在于开启协程的方法StartCoroutine()只能在继承了MonoBehavior的类中使用,而cd判定节点必须继承自Behavior,且C#不支持多继承。
最后从网上找到大佬的解决办法。在场景中创建一个空物体(MonoStub),然后在空物体上挂载一个继承自MonoBehavior的空脚本MONOStub.cs ,最后利用MonoStubTemp.GetComponent<MONOStub>().StartCoroutine());语句调用StartCoroutine();
完整脚本在这里:
using System.Collections;
using UnityEngine; public class CDNode : Behavior{ private float cd; private bool isCoolingDown; public CDNode(float cd){ this.cd = cd; this.isCoolingDown = false; } protected override EStatus OnUpdate(){ if (isCoolingDown) return EStatus.Failure; MStartCoroutine(); return EStatus.Success; } private void MStartCoroutine(){ GameObject MonoStubTemp = GameObject.Find("MonoStub"); if (MonoStubTemp == null){ MonoStubTemp = new GameObject(); MonoStubTemp.name = "MonoStub"; MonoStubTemp.AddComponent<MONOStub>(); } MonoStubTemp.GetComponent<MONOStub>().StartCoroutine(CoolDown(cd)); //Debug.Log("开始计时器协程"); } IEnumerator CoolDown(float cd){ isCoolingDown = true; yield return new WaitForSeconds(cd); isCoolingDown = false; }} public partial class BehaviorTreeBuilder{ public BehaviorTreeBuilder CDNode(float cd){ var node = new CDNode(cd); AddBehavior(node); return this; }}
3.构建树
构建树就比较简单了,参照模拟图按部就班的写就行,这里我学着大佬写的有层次一点
private void BuildTree(){ builder.Seletctor() .Sequence() .DeidTrigger() .Died(anim, rb) .Back() .Sequence() .SkillTrigger() .ChangeDirection(rb) .Skill(anim, rb) .Back() .Sequence() .CDNode(5) .AttackTrigger(rb) .ChangeDirection(rb) .Attack(rb, anim) .Back() .Sequence() .CDNode(5) .WatchTrigger(rb) .ChangeDirection(rb) .Track(rb, anim) .ChangeDirection(rb) .Back() .Sequence() .Idle(anim) .Back() .End();
}
将这个方法放在start()中运行,然后将builder.TreeTick();放在Update()中即可
private void Start(){ BuildTree();
} private void Update(){ builder.TreeTick();
}
三、总结
相比于有限状态机,行为树在实现较为复杂的AI逻辑时具有很大的优势,行为节点和条件节点的组合使用极大地提升了代码的复用性,也使一些多阶段动作的实现更容易了。
那么之后我应该会去继续学习分层任务网络(HTN),据说是比行为树更先进一些,学习更先进更高级的东西使我快乐。
最终效果演示:使用行为树重新设计假骑士的行为逻辑_哔哩哔哩_bilibili
项目源码:使用行为树重新设计假骑士的行为逻辑_哔哩哔哩_bilibili