《行为树究竟是个啥|社区征文》

浅谈一下下行为树(一)

一、什么是行为树

行为树,英文是Behavior Tree,简称BT,是一棵用于控制 AI 决策行为的、包含了层级节点的树结构

1. 原理: 遍历

当我们要决策当前三哥要做什么样的行为的时候,我们就会自顶向下的通过一些条件来搜索这颗树,最终确定需要做的行为(叶节点),并且执行它,这就是行为树的基本原理。

这并不是一个很有效率的方式,尤其是当你的树变得非常深的时候。我认为行为树的实现必须具备可以在一定的tick内时间完成整个行为树的判断逻辑。

2. 实现方式: 工作流

行为树由多种不同类型的节点构成,它们都拥有一个共同的核心功能,即它们会返回三种状态中的一个作为结果。这三种状态分别是:

  • 成功-Success;
  • 失败-Failure;
  • 运行中-Running;

前两种行为正如它们的名字,是用来向它们的父节点通知运行的成功或失败的结果。第三种是指还在运行中,结果还未决定,在下一个 tick 的时候再去检查这个节点的运行结果。这个功能非常重要,它可以让一个节点持续运行一段时间来维持某些行为。

比如一个让角色保持某个状态的过程中持续返回“Running”来让角色保持这一状态。

这些状态可以用来决定行为树的走向,确保 AI 可以按照我们预期的方式来以某些顺序去执行行为树里的行为。

二、组成部分

行为树主要由以下四种节点抽象而成组合节点装饰节点条件节点行为节点

1. 组合节点(Composites)

主要包含:Sequence顺序节点,Selector选择节点,Parallel并行节点以及他们之间相互组合的条件。

// 组合节点的基类
export class Composite extends BaseNode {
    constructor(children: BaseNode[] = []) {
        super();
        this.children = children;
    }
}
  • ① Sequence顺序节点

正如它的名字所说,次序节点会依次(通常是从左到右)访问子节点。每个子节点成功之后便轮到下一个,直到最后。如果所有子节点都 Success,则向次序节点返回 Success;其间任何一个子节点返回 Failure,就会立即向次序节点返回 Failure 的结果。

Sequence(顺序节点)有很多的用处,其中最显而易见的用法就是执行一连串有前后依存关系的行为,其中一个的失败必然导致后续的动作没有进行的意义,比如这个“三哥成为神”行为的例子:

image

这个顺序节点(Sequence)下所有的子节点共同让三哥实现了从魂师到神的连串动作。过程如下:

顺序节点(该阶段的根节点) ->初次觉醒成为魂师(Success) -> 猎杀魂环提升魂师等级(Success) -> 通过海神九考成为神(Success),次序节点的父节点返回 Success。

如果三哥因为某些原因未能成功觉醒魂力成为魂师,那么试图成为海神的行为都没有意义了。当觉醒魂力这个动作失败的时候,次序节点就会返回 Failure,其父节点就可以根据这个结果来进行后面的事情了。

export class Sequence extends Composite {
    private _runningIndex = 0;
    onOpen() {
        this._runningIndex = 0;
    }
    onTick() {
        // 依次从左到右执行子节点,直到子节点返回Failure而终止。
        for (let i = this._runningIndex; i < this.children.length; i++) {
            let status = this.children[i].tick();
            if (status == Status.SUCCESS) {
                continue;
            }
            if (status === Status.RUNNING) {
                this._runningIndex = i;
            }
            return status
        }
        return Status.SUCCESS;
    }
}

顺序节点(Sequence)除了非常自然地用于进行一系列前后依存的动作之外,还可以用来做一些其他的事情,比如又先后关系的条件判断行为。

  • ② Selector选择节点

选择节点有称优先级节点,会从左到右依次执行所有的子节点,只要子节点返回failure,就执行后续的子节点, 直到有一个节点返回success或running为止,这时会停止后续子节点的执行,向父节点返回success或running,若所有子节点都返回failure,那么他会向父节点返回failure

它的主要作用在于它可以用来表示一个行为的多种方式,从最高优先级到最低,任何一个方式的成功都会让这个动作 Success。

比如这个“三哥在10级的时候选择魂环”行为的例子

image

选择节点(该阶段的根节点) -> 千年魂兽的魂环(Success) -> 结束后续节点的行为,选择节点点的父节点返回 Success。

选择节点(该阶段的根节点) -> 千年魂兽的魂环(Failure) ->-> 百年魂兽的魂环(Failure) -> 十年魂兽的魂环(Failure),次序节点的父节点返回 Failure。

export class Selector extends Composite {
    private _runningIndex = 0;
    onOpen() {
        this._runningIndex = 0;
    }

    onTick() {
        let index = this._runningIndex;
        for (let i = index; i < this.children.length; i++) {
            let status = this.children[i].tick();
            if (status == Status.FAILURE) {
                continue;
            }
            this._runningIndex = i;
            return status
        }
        return Status.FAILURE;
    }
}

有细心的小伙伴已经发现了,上面介绍的这种选择节点每次TICK的时候顺序都是固定的,然而有时候这种顺序节点满足不了我们的需求,这是可以派生出随机选择节点、带优先级的选择节点等类型的选择节点,本文在此不做过多的扩展和描述。

  • ③ Parallel并行节点

让所有子节点同时运行,那它什么时候结束呢,可以使当所有子节点都完成的时候结束或者也可以让任一子节点完成时结束,视乎需要来做出选择。

比如这个“三哥在吸收魂环魂骨”行为的例子


// 当所有子节点都完成的时候结束
export class ParallelAll extends Composite {
    private count: number = 0;
    private _closedMap = {};

    onOpen() {
        this.count = 0;
        this._closedMap = {};
    }

    onTick() {
        if (this.count >= this.children.length) {
            return Status.SUCCESS
        }
        for (let i = 0; i < this.children.length; i++) {
            let child = this.children[i];
            if (this._closedMap[child.id]) continue;
            let status = child.tick();
            if (status === Status.SUCCESS) {
                this.count++;
                this._closedMap[child.id] = true;
                continue
            }
            if (status === Status.FAILURE) {
                return Status.FAILURE
            }
        }
        return Status.RUNNING;
    }
}

2.装饰节点(Decorator)

连接树叶的树枝,就是各种类型的修饰节点,这些节点决定了 AI 如何从树的顶端根据不同的情况,来沿着不同的路径来到最终的叶子这一过程。
如让子节点循环操作(LOOP)或者让子task一直运行直到其返回某个运行状态值(Util),或者将task的返回值取反(NOT)等等,下面举几个常用的装饰节点(Decorator)来举例说明什么是装饰节点。

export class Decorator extends BaseNode {
    constructor(protected child: Action) {
        super();
        this.children = [child];
    }
}
  • ① 逆变节点(Inverter)

他会反置或否定子节点的结果。如果子节点返回SUCCESS,则返回FAILURE,反之子节点返回FAILURE, 则返回SUCCESS

export class Inverter extends Decorator {
    protected onTick(): Status {
        let status = this.child.tick();
        if (status === Status.SUCCESS) {
            status = Status.FAILURE;
        } else if (status === Status.FAILURE) {
            status = Status.SUCCESS;
        }
        return status;
    }
}
  • ② 成功节点(Until Succee)

成功节点不管它的子节点向其返回的结果为何,它总是返回 Success 的结果。这个往往用在当你知道一个子节点一定会返回 Failure 的结果,而它的父节点是次序节点,会因此而终止,那么你可以强行让这个子节点返回 Success,来避免这一情况的发生。我们并不需要一个专门的失败节点,因为一个逆变节点加上成功节点就可以达到这一效果。

export class UntilSuccess extends Decorator {
    constructor(params) {
        super(params);
    }

    protected onTick(): Status {
        let status = this.child.tick();
        if (status == Status.FAILURE) {
            return Status.SUCCESS;
        }
        return status;
    }
}
  • ③ 重复直至失败节点(Repeat Until Fail)

类似重复节点重复执行子节点,但这一节点会在子节点 Failure 的时候返回 Failure

  • ④ 重复节点(Repeater)

重复节点会在它的子节点返回结果后反复继续执行它。重复节点常常被用在一棵树的最顶部来确保树的持续运行。另外重复节点也可以被设定重复执行的次数。

 export class Limite extends Decorator {
    constructor(child: Action, private maxLoop: number = 1) {
        super(child);
    }

    private _count: number = 0;

    protected onOpen(): void {
        this._count = 0;
    }

    protected onTick(): Status {
        if (this._count >= this.maxLoop) {
            return Status.FAILURE;
        }
        let status = this.child.tick();
        this._count += 1;
        return status;
    }
}

3.条件节点(Conditinals)

用于判断某条件是否成立。目前看来,是Behavior Designer为了贯彻职责单一的原则,将判断专门作为一个节点独立处理,比如判断某目标是否在视野内,其实在攻击的Action里面也可以写,但是这样Action就不单一了,不利于视野判断处理的复用。一般条件节点出现在Sequence控制节点中,其后紧跟条件成立后的Action节点。

4.行为节点(Action)

行为节点是真正做事的节点,行为节点在树的最末端,都是叶子节点,就是这些 AI 实际上去做事情的命令;

5.黑板(Blackboard)

一种数据集中式的设计模式,一般用于多模块间的数据共享

如果你对所分享的内容感兴趣,可以关注微信公众号【 游戏讲坛 】。
游戏讲坛已开通微信公众号交流群。如想加入请关注微信公众号。
image

6赞

:rofl:看不懂,看不懂,魂兽是啥,三哥是谁,太深奥了,这篇文章

:joy:
一脸懵地进来, 看了好一会, 还是看不太明白, 估计要是有案例可能更好理解,
不过还是感谢大佬的分享