决策模型-BehaviourTree

一.行为树是什么

行为树(Behavior Tree)是一种用于建模和实现复杂行为的结构,广泛应用于游戏开发、机器人控制和人工智能等领域。我们在游戏开发时有时会利用行为树来设计具有复杂决策的智能AI,因为它提供了一种清晰、可扩展的方式来组织和管理角色或系统的行为。


二.行为树的优点

1. 可复用性

行为树的节点可以被复用,这意味着同一个行为可以在不同的上下文中使用。这种复用性使得行为树在复杂系统中的管理和扩展变得更加高效。

2. 结构清晰

行为树通常可以通过图形化工具(或者直接梳理决策脉络得到结构图)进行可视化,便于开发者和设计师理解、修改和调试角色的行为。其清晰的结构不会造成像状态机中某些“交际花“状态,让你眼花撩乱的情况。

3. 易于扩展

由于行为树的层次结构和模块化设计,当需要添加新行为或修改现有行为时,开发者可以轻松地插入或替换节点,而无需重构整个系统。

4. 状态管理

行为树能够有效地管理同一状态下的决策切换,也可以允许系统在不同的之间切换。例如,角色可以在“巡逻”、“追逐”和“攻击”状态之间切换,而每个状态都有其特定的行为树。


三.行为树的原理

1. 结构

行为树由节点组成,节点主要分为以下几类:

叶子节点(Action Nodes):每片叶子记录一种策略,执行具体的行为,如移动、攻击、播放动画等行为逻辑。

条件节点(Condition Nodes):检查某些条件是否满足,以决定是否执行某个行为。

控制节点(Control Nodes):负责控制行为的执行流程,主要包括

   并行节点(Parallel Node):同时执行多个子节点,根据设定的条件决定成功或失败。

   顺序节点(Sequence Node):按顺序执行子节点,直到一个子节点失败

   选择节点(Selector Node):按顺序执行子节点,直到一个子节点成功

2. 执行流程

#行为树的执行是自上而下(可以理解从根部出发,延至尽头的一片叶子上,这一条通路就是一种决策)的。控制节点会根据其子节点的状态(成功Success、失败Faliure、运行中Running)来决定接下来的执行步骤。行为树的执行允许动态调整行为。

#当你的节点内存在优先级时,会有打断关系,指当AI行为发生变化,行为树的高优先级节点会打断低优先级节点的执行。
(注:这里的打断关系包括:打断自身,打断低优先级,打断自身及低优先级,不打断)

行为树的执行通常遵循以下步骤:

  1. 启动执行:从根节点开始执行,根节点会根据其类型决定如何处理其子节点。

  2. 节点评估:每个节点在执行时会被评估其状态,通常有三种状态:

    • 成功(Success):节点成功完成其任务。
    • 失败(Failure):节点未能完成其任务。
    • 运行中(Running):节点正在进行中,尚未完成。
  3. 状态传播:控制节点会根据其子节点的状态来决定自身的状态。例如:

    • 在顺序节点中,如果一个子节点失败,顺序节点立即返回失败;如果所有子节点成功,顺序节点返回成功。
    • 在选择节点中,如果一个子节点成功,选择节点返回成功;如果所有子节点失败,选择节点返回失败。
  4. 状态更新:行为树的状态可以在每一帧更新,允许角色的行为根据环境变化动态调整。


四.尝试实现

1.思路梳理:

我们先设计一下各个组成部分的功能 以便 推出可能需要的字段或方法

0.Status(节点状态的枚举):

用来描述节点的执行状态进程

enum Status {  Running,Success,Faliure  }

1.Node(节点基类):

1.记录本节点的名称                 =>      string name

2.记录自身的所有子节点          =>      list<Node> children

3.记录执行中当前节点的序号   =>      int  currentChild

4.记录本节点的优先级              =>      int priority

5.描述当前节点自身的状态       =>      Status  status

6.添加子节点的方法                  =>      void  AddChild(Node child)

7. 获取当前状态更新的方法      =>      Status  Process()

8.回溯方法 (当前的策略被打断时,重新开启对子节点的遍历)   =>   void Reset()

(可选,主要是考虑后面控制节点里的选择节点(Selector)的功能优化)

2.ConditionNode (条件节点)

1.具有一个判断方法用于进入节点的逻辑

2.具有打断当前序列节点执行的逻辑 

3.ControlNode (控制节点)

1.Sequence Node:

按顺序执行子节点,直到一个子节点失败

2.Selector Node:

按顺序执行子节点,直到一个子节点成功

* Parallel Node:同时执行多个子节点,(在unity里用的较少,本人不是很熟悉,先不过多介绍)

4.LeafNode(叶节点)

1.记录本叶节点的策略  ,我们这里选择让这些节点继承一个接口 IStrategy 

(我们就是通过这个接口变量来为叶节点分配实现 状态更新方法)

2.代码实现:

0.Status(节点状态枚举)

public enum Status 
{
  Running,  //节点运行中
  Success,  //节点运行成功
  Failure,  //节点运行失败
}

1.Node(节点基类)

public class Node 
{

    public Status status;    //节点当前状态
    public  List<Node> children=new List<Node>();//子节点列表
    public int priority;     // 优先级
    public int currentChild; //当前执行的节点序号
    public string name;      //节点名称
    //使用构造函数进行初始化
    public Node(string  name,int priority=0) 
    {
     this.name = name;
     this.priority = priority;
    }
    //添加子节点
    public virtual void AddChild(Node child) => children.Add(child);
    //状态更新
    public virtual Status Process() => children[currentChild].Process();
    //重置回溯
    public virtual void Reset() 
    {
     currentChild = 0; //返回初始节点重新执行
     children.ForEach(child => child.Reset());
    }
}

2.BehaviourTree(行为树脚本)

可以理解是一个异化的Sequence,当前子节点必须Succes才能进入下一子节点

public class BehaviourTree : Node
{
    public BehaviourTree(string name) : base(name) { }

    public override Status Process()
    {
        while (currentChild < children.Count)
        {
            var status = children[currentChild].Process();
            if (status != Status.Success)
            {
                return status;
            }
            //当前节点成功,再进入下一节点
            currentChild++;
        }
        return Status.Success;
    }
}

3.Condition(条件节点)

首先实现策略接口 IStrategy

public  interface IStrategy 
{
    Status Process();
    void Reset() { }
}

一般条件节点作为一种修饰节点,是只有一个子节点的,所以我们不妨直接不继承Node,直接来做内部的判断逻辑,Condition 节点实现代码如下

public class Condition : IStrategy
{
    readonly Func<bool> predicate;   //判断逻辑
    public Condition(Func<bool> predicate)
    {
        this.predicate = predicate;
    }
    //满足条件才能进入下面的节点,否则就打断
    public Status Process() => predicate() ? Status.Success : Status.Failure; /
}

4.ControlNode(控制节点)

1.Sequence Node(序列节点):
public class SequenceNode : Node
{
    public SequenceNode(string name, int priority = 0) : base(name, priority) { }
    public override Status Process()
    {
        //遍历子节点
        if (currentChild < children.Count)
        {
            switch (children[currentChild].Process())
            {
                case Status.Running:
                    return Status.Running;
                case Status.Failure: 
                    Reset();   //如果找到有运行失败的子节点,会直接打断Sequence节点的执行
                    return Status.Failure;
                default:
                    currentChild++;
                    return currentChild == children.Count ? Status.Success : Status.Running;
            }
        }
        Reset();
        return Status.Success;
    }
}
2.Selector Node(选择节点):
public class SelectorNode : Node
{
    public SelectorNode(string name) : base(name) { }
    public override Status Process()
    {
        if (currentChild < children.Count)
        {
            switch (children[currentChild].Process())
            {
                case Status.Running:
                    return Status.Running;
                case Status.Success:
                    Reset();   //如果发现有运行成功的节点,会打断Selector执行并进入该子节点
                    return Status.Success;
                default:
                    currentChild++;
                    return  Status.Running;
            }
        }
        //如果没有找到运行成功的子节点,返回失败并重置重新遍历子节点
        Reset();
        return Status.Failure;
    }
}
3.PrioritySelector Node(优先级选择节点):

(优化Selector,依据各个子节点的优先级进行选择)

public class PrioritySelectorNode:Node
{
    List<Node> sortedChildren;
    public PrioritySelectorNode(string name, int priority = 0) : base(name, priority) { }
    List<Node> SortedChildren =>  sortedChildren ??= SortCildren();
    // ??=:检查左侧的变量是否为null,如果是则将右侧的值赋给左侧的变量
    protected virtual List<Node> SortCildren()=>children.OrderByDescending(child=>child.priority).ToList();
    //OrderByDescending(child=>child.priority)表示依据数据child的priority字段来进行升序排列,并返回该升序列表
    public override void Reset()
    {
        base.Reset();    
        sortedChildren = null;
    }

    public override Status Process()
    {
        foreach (var child in SortedChildren) 
        {
            switch (child.Process())
            {
                case Status.Running:
                    return Status.Running;
                case Status.Success:
                    return Status.Success;
                default:
                    continue;
            }
        }
        Reset() ;
        return Status.Failure;
    }
}

5.LeafNode(叶节点)

public class LeafNode : Node,IStrategy
{
    readonly IStrategy strategy; //自身的策略
    public LeafNode(string name, IStrategy strategy, int priority = 0) : base(name, priority)
    {
        this.strategy = strategy;
    }
    public override Status Process()=>strategy.Process();
    public override void Reset()=>strategy.Reset();

}

实际运用

既然我们已经实现了基本的节点,接下来我们就尝试应用我们的行为树来实现一种简单的AI行为吧!

1.实验目标:

创造一个测试AI的控制器脚本Enemy,实现当在场景中有激活的Player时,打印目标信息,当有多个Player目标,则选择优先级更高的Player,打印目标信息

2.代码实现:

public class Enemy : MonoBehaviour
{
    BehaviourTree tree;

    public GameObject Player1;
    public GameObject Player2;
    private void Awake()
    {
        tree = new BehaviourTree("Enemy");

        Sequence goToPlayer1 = new Sequence("ToPlayer1",2);
        goToPlayer1.AddChild(new Leaf("PreasentPlayer1", new Condition(() => Player1.activeSelf)));
        goToPlayer1.AddChild(new Leaf("MoveToPlayer1", new ActionStrategy(() => {  Debug.Log("我发现了玩家1,我打!"); })));

        Sequence goToPlayer2 = new Sequence("ToPlayer2",1);
        goToPlayer2.AddChild(new Leaf("PreasentPlayer2", new Condition(() => Player2.activeSelf)));
        goToPlayer2.AddChild(new Leaf("MoveToPlayer2", new ActionStrategy(() => {  Debug.Log("我发现了玩家2,我打!!"); })));
      
        PrioritySelector playerSelector = new PrioritySelector("playerSelector");//子节点node的priority越大优先级越高

        playerSelector.AddChild(goToPlayer1);
        playerSelector.AddChild(goToPlayer2);

        tree.AddChild(playerSelector);
    }
    private void Update()
    {
        tree.Process();
    }

}

这里的ActionStrategy只是举例的一种示例策略,在其他应用场景你可以创建自定义的策略(比如某一状态的不同策略,以巡逻为例,patrol状态下就可能有idle巡视和walk巡视两种不同的策略)

public class ActionStrategy:I_Strategy 
{
    readonly Action doSomthing; //接收含具体逻辑的方法
    public ActionStrategy(Action doSomthing) 
    {
    this.doSomthing = doSomthing;
    }
    public Node.Status Process()
    {
        doSomthing();
        return Node.Status.Success;
    }
}

在编辑器内新建空物体添加测试脚本,并创建两个游戏物体分别代表Player1和Player2

3.测试结果:

1.当开始运行时  Player1和Player2均失活,无打印信息,

将Player2 激活,打印“我发现了玩家2,我打!!” 

2.当开始运行时  Player1和Player2均失活,无打印信息,

将Player1激活,打印“我发现了玩家1,我打!!” 

3.当开始运行时 Player1和Player2均激活,则会选择优先级更高的玩家一节点,打印“我发现了玩家1,我打!!”

可以见到实验目标基本实现!

注:如果你想实现在几个节点间循环切换的效果,请在对应的策略中实现打断逻辑,使达成某些条件的情况下从叶节点返回并进入新的叶节点。后面我还会写一篇具体实现行为树回溯节点的应用。

尾声

这几天正巧国庆假期 (^~^),但学习动力还是得有!几天后还要忙一阵子准备比赛了,希望自己能有成长吧,大家也要加油成长哦,诸君共勉!

本篇丸~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值