行为树简介

2021-02-06  本文已影响0人  太刀
美女镇楼

行为树是实现游戏AI的一个重要方案,本文主要介绍行为树的理论基础,并通过实现一些简单示例来加深理解。本文的理论和示例基于著名的 Unity 插件 Behaviour Designer 而来,并参考了腾讯的行为树开源方案 behavic

1. 什么是行为树

1.1 决策模型

我们在开发游戏过程中通常需要为游戏中的非玩家控制主体,如小怪、Boss等,附加一些AI行为,核心是根据当前的游戏世界状态信息,进行决策,确定主体的行为并对行为状态进行监控,例如 Boss 根据自身血量范围、玩家的血量范围,确定是逃跑还是释放技能。下面这张图展示了一个行为决策模型。

游戏AI架构

1.2 状态机

有限状态机(FSM,Finite State Machine)是最简单最经典的行为决策模型,主体绑定一个包含若干状态的 FSM,任何时候主体必处于若干状态中的一种,当外界条件发生变化时,检查 FSM,看看当前状态在接受该条件时将会转换到哪一个状态,将主体切换到该状态下。
有限状态机作为决策模型存在以下的问题:

1.3 行为树

行为树的决策方式基于他的树形数据结构,在需要进行决策时,从树的根节点出发,按照一定顺序遍历子节点,遍历的过程中进行一系列的条件判断,决策得到最后的行为节点并执行。下面的示例是一个很简单的行为树,它描述的决策逻辑是:当自身血量低于 40% 时逃跑,否则进行攻击。这里先对行为树有个整体大概的印象,暂不必纠结于中间各种节点的作用。

行为树示例

2. 行为树中的节点类型

2.1 三大类节点

行为树由若干节点组成,节点可以分为三大类:根节点、控制类节点和行为类节点

2.2 节点返回值

行为树中每个节点都有返回值。叶子节点的返回值由它绑定的条件或行为方法确定,非叶子节点的返回值由它的子节点的返回值根据一定规则来确定,返回值的类型有三种:

2.3 行为类节点

行为类节点通常与游戏世界状态相关,其中条件节点Conditional通常读取游戏世界的某个状态,动作节点Action 执行游戏世界的某个动作。
使用 Behaviour Designer为场景中的空 GameObject 新建一个 Behaviour Tree 如图

SingleAction
这个行为树的根节点直接连接一个 Action 作为子节点,该子节点的作用是打印日志,保存行为树,运行场景,该 GameObject 确实驱动行为树打印了一行日志
BehaviourDesigner 内置了一系列的 Action 节点,包括:

BehaviourDesigner 内置的Conditionals节点必须包含 Bool 类型的返回值,Conditionals 节点包含

2.4 控制类节点

控制类节点和游戏世界无关,聚焦行为树框架本身的控制逻辑,通常是用来控制和管理子节点,并根据子节点的返回结果来确定自己的返回结果。根据子节点的数量,可以将控制类节点分为复合节点Composites装饰节点Decorators

2.4.1 复合节点

复合节点可以包含1个或多个子节点,常见的复合节点类型包括序列、选择和并行等。

可以扩展自己的复合节点,来实现特定的返回值控制逻辑。如下代码扩展了一个具有 if-else 逻辑的复合节点

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class IfElse : Composite
{    
    private int currentChildIndex = 0;    
    private TaskStatus executionStatus = TaskStatus.Inactive;

    public override int CurrentChildIndex()
    {
        return currentChildIndex;
    }

    public override bool CanExecute()
    {
        if (currentChildIndex == 0)
        {
            return true;
        }

        if (executionStatus == TaskStatus.Success)
        {
            return currentChildIndex == 1;
        }
        else
        {
            return currentChildIndex == 2;
        }        
    }

    public override void OnChildExecuted(TaskStatus childStatus)
    {        
        if (currentChildIndex == 0)
        {
            if (childStatus == TaskStatus.Success)
            {
                currentChildIndex = 1;
            } else
            {
                currentChildIndex = 2;
            }
        }
        else
        {
            // if 或 else 分支的节点返回了,整个节点就返回了
            currentChildIndex = 99;
        }        
        executionStatus = childStatus;
    }

    public override void OnConditionalAbort(int childIndex)
    {        
        currentChildIndex = childIndex;
        executionStatus = TaskStatus.Inactive;
    }

    public override void OnEnd()
    {     
        executionStatus = TaskStatus.Inactive;
        currentChildIndex = 0;
    }
}

该组合节点有3个子节点,第一个子节点为条件节点,组合节点的逻辑是:当第一个条件子节点返回 Success时,执行第二个子节点,否则执行第三个子节点。当设置第一个条件子节点返回值为 Failure 时的执行情况:


If-else

修改条件子节点,使其返回 Success,执行情况为


if-else 节点.png
2.4.2 装饰节点

装饰节点只能有一个子节点,装饰节点的作用是针对这个子节点的执行逻辑和返回值进行一些限制和调整。

当指定一个子节点返回为 Success的情形如下图,尽管子节点返回 Success,但装饰节点还是返回了 Failure


子节点返回成功

2.5 扩展节点

大多数的行为树框架都提供了节点的扩展方案,你可以定制自己的 Actions/Conditionals/Decorators/Composites 节点,这里以扩展一个打印当前帧数的动作节点为例,描述如何在 BehaviourDesigner 中进行节点的扩展

2.5.1 首先实现脚本来计算当前帧数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BehaviourController : MonoBehaviour
{
    private int _frameCount;

    public int frameCount
    {
        get
        {
            return this._frameCount;
        }

        set
        {
            this._frameCount = value;
        }
    }
    
    void Start()
    {
        this._frameCount = 0;        
    }

    // Update is called once per frame
    void Update()
    {
        this._frameCount += 1;
    }
}
2.5.2 扩展 Action 类,实现 FrameCountAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

[TaskDescription("打印当前的帧数")]
[TaskIcon("{SkinColor}LogIcon.png")]
public class FrameCountAction : Action
{
    public override TaskStatus OnUpdate()
    {
        // Log the text and return success
        BehaviourController controller = this.gameObject.GetComponent<BehaviourController>();
        Debug.Log("frameCount:" + controller.frameCount);
        return TaskStatus.Success;
    }

    public override void OnReset()
    {
    }
}

要点包括

2.5.3 其它节点的扩展

Conditionals/Composites/Decorators 节点的扩展方式非常类似,集成对应的类,实现对应方法即可,Composites 类的扩展可能相对复杂一点,这里提供了一个具备 if-else 功能的复合节点扩展,代码如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using BehaviorDesigner.Runtime.Tasks;

public class IfElse : Composite
{    
    private int currentChildIndex = 0;    
    private TaskStatus executionStatus = TaskStatus.Inactive;

    public override int CurrentChildIndex()
    {
        return currentChildIndex;
    }

    public override bool CanExecute()
    {
        // 条件节点必然可以执行
        if (currentChildIndex == 0)
        {
            return true;
        }

        // 条件节点成功时执行 if 分支
        if (executionStatus == TaskStatus.Success)
        {
            return currentChildIndex == 1;
        }
        else
        {
            // 否则执行 else 分支
            return currentChildIndex == 2;
        }        
    }

    public override void OnChildExecuted(TaskStatus childStatus)
    {        
        if (currentChildIndex == 0)
        {
            // 条件节点执行完成后根据执行结果确定下一个执行的节点
            if (childStatus == TaskStatus.Success)
            {
                currentChildIndex = 1;
            } else
            {
                currentChildIndex = 2;
            }
        }
        else
        {
            // if 或 else 分支的节点返回了,整个节点就返回了
            currentChildIndex = 99;
        }        
        executionStatus = childStatus;
    }

    public override void OnConditionalAbort(int childIndex)
    {        
        currentChildIndex = childIndex;
        executionStatus = TaskStatus.Inactive;
    }

    public override void OnEnd()
    {     
        executionStatus = TaskStatus.Inactive;
        currentChildIndex = 0;
    }
}

这里需要特别注意的是 OnChildExecutedCanExecute 方法的实现,可以参考内置的各种 Composites 都是如何实现的

3. 行为树运行逻辑

3.1 行为树多久执行一次?

我们知道,每次行为树执行是从根节点开始遍历子节点执行,这样的一次遍历称为一个 tick,通常情况下,每一帧会 tick 一次,这样可能会有性能的损耗,而实际上很多怪物其实不需要如此敏感和密集的决策,可以优化行为树的 tick 频率。在 BehaviourDesigner 中,在行为树返回结果非 Running 时,tick 一下行为树就立即结束了,除非你勾选了 Restart When Complete 选项

完成时重启
当然,如果行为树根节点返回值为 Running,则每一帧都会 tick

3.2 返回值为 Running 的行为树执行机制

返回值为 Running 的行为树,估计是保存了 Running 的节点,每一次 tick 都直接执行该 Running 节点,当然,如果有复合节点设置了打断条件,那应该每一个 tick 都会执行该复合节点的条件子节点来判断打断条件是否满足

4. 节点运行状态的打断

复合节点可以设置打断类型,行为树每次 tick 时会检测设置打断类型的子条件节点,当条件的返回值发生变化时,会中断正在 Running 状态的节点,立即执行条件子节点并返回。
可以设置的打断类型包括:

4.1 Self:打断子节点

我们这里新建一个行为树,添加一个 Selector 节点,为其添加一个条件子节点,判断条件是鼠标是否按下,再添加一个动作节点 Wait,等待时间设置为 10000 秒,我们知道该行为树运行时,由于鼠标未按下,将会返回 Wait 的 Running 状态。


行为树

当我们按下鼠标时

4.2 Lower Priority 打断低优先级节点

当按下鼠标时,Sequence 的条件子节点发生变化,所有打断了 Running 状态的 Sequence 的兄弟节点,Selector 的第二个子节点 Wait 的 Running 状态被打断,重新检查 Sequence 的条件子节点,返回 Success,所以进入了 Sequence 子节点 Wait 的 Running 状态,如动图所示


打断兄弟节点

5. 数据共享

行为树不同节点之间传递数据,节点间共享全局数据的逻辑,可以参考这篇文章
BehaviourDesigner中的变量

上一篇 下一篇

猜你喜欢

热点阅读