20201218 UE4 Behavior Tree Abort Mechanism UE4行为树Abort机制研究与其他Decortor使用细则

疑问

选中一个Decorator节点的时候,Detail面板中有一个Observe aborts,并有如下选项:

  • None
  • Self
  • Lower Priority
  • Both

现有疑问:

  1. Self与Both区别如何?目前猜测:Both会继续执行接下来的节点,但是直接执行其abort回调函数
  2. Abort在Sequence和Selector中表现如何?会继续执行接下来的逻辑吗?
  3. Decorator是否与Service一样,全局共用同一个实例?如果是,如何做数据初始化?

Sequence

在Sequence中,一旦任务被abort,相当于运行失败,不会再运行下一个节点。

当decorator修饰的是Sequence下的任务节点时,你会发现只能够选择NoneSelf

Selector

Self

如果选择了Self,在不满足Decorator时,会触发Self涵盖范围下(当前分支下)任务的abort,你可以在任务的Abort函数下处理中断操作。

选择Self并且被abort之后,selector的下一个子节点会被正常执行。

如上图,Decorator A选择了Self,那么当A任务被abort时,可以认为任务A执行失败,继而尝试执行任务B。

Lower Priority

如果选择了Lower Priority,那么在不符合decorator条件时,并不会强硬地调用当前任务的Abort,而是会当做无事发生,使当前任务正常地结束。

待当前节点正常结束之后,继而调用下一个节点的Abort。猜测这是为了让接下来的节点知道前面发生了事故,没办法到自己这儿来了,需要处理一下现场。

Both

以上两者的和。

举个例子

比如一个select规定:要么吃A,要么吃B,优先吃A(顺序在前)。其中吃A有一个装饰器,标记得角色肠胃健康才能吃(食物中毒就不能吃东西了)。主角在吃A的过程中,突然肚子疼,这个时候:

  • 如果选择了abort self,那么主角认为肚子疼是因为A这个东西有问题,但是又很饿,所以决定不吃A了,转而吃接下来的B
  • 如果选择了abort lower priority,那么主角是一个节约粮食的人,忍着肚子痛也要把A吃完。也许怕别人误认为B有毒,又也许怕这次上过厕所后忘记回来吃B了,主角把B上面做了个标记(调用了B的abort)。

单例的问题

看到官方文档说同一个树里面的同一个decorator或同一个service,都是共享同一个实例的。也就是如果你在decorator或者service里面定义了一些成员变量并且用上的时候,你可能会以为不同的decorator节点之间的数据应该是相互隔离的,但是其实不是。上一次使用之后遗留的数据会影响下一次的运算。

解决方法有二:

  • 每次使用之前都重新获取一次数据
  • 使用「官方建议的方案」

官方建议的方案是:

每个节点会在行为树中注册一块内存,用以存放成员数据。

首先如果你需要定义一些成员变量,那么在decorator或者service的前面定义一个结构体,用以存放这些数据。结构体的名字可以是FBTXXXDecoratorMemory。参考BTDecorator_Cooldown.h的源码,它定义了一个结构体叫FBTCooldownDecoratorMemory

为了能够把这些个结构存到行为树里,首先要行为树开辟一个内存空间。这个内存空间需要多大,需要我们去重写GetInstanceMemorySize,一般来说,是返回sizeof()你的结构体,比如:

1
2
3
4
uint16 UBTDecorator_Cooldown::GetInstanceMemorySize() const
{
return sizeof(FBTCooldownDecoratorMemory);
}

讲道理行为树执行这个方法之后,就知道要开辟一块多大的空间了,开辟空间之后,会把指针保存下来。

UBTNode自带的很多函数,比如InitializeMemoryOnNodeDeactivationTickNode等,你会发现都有一个参数叫做NodeMemory的,这个就是开辟的空间的指针(不准确地说,因为指针其实应该是int64的)。

使用如下语句可以把NodeMemory转换为对应的结构体,从结构体中可以读取出对应的成员变量:

1
FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);

成员变量初始化

重写InitializeMemory可以实现对成员变量结构体的初始化,作用相当于实例初始化的构造函数。比如在Cooldown的装饰器中,拿到成员变量结构体之后就可以进行初始化:

1
2
3
4
5
6
7
8
9
10
void UBTDecorator_Cooldown::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const
{
FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);
if (InitType == EBTMemoryInit::Initialize)
{
DecoratorMemory->LastUseTimestamp = -FLT_MAX;
}

DecoratorMemory->bRequestedRestart = false;
}

对于每一个节点,都会有其对应的一块内存。但是同一个节点在不同次进来,都是使用的同一块内存。

比如在一个selector下有两个task,分别装饰一个cooldown,那么Cooldown A与Cooldown B分别拥有不同的一块成员变量内存,但是第一次执行Cooldown A和第n次执行Cooldown B是使用的同一块内存。

如果要使得同个节点不同次进来的数据是全新的,那么可以重写OnNodeDeactivation函数,在这里面将成员变量进行重置。

C++对应的蓝图事件是ReceiveExecutionFinishAI

触发条件的检测

在Cooldown中,检测到如果abort类型不为None,那么bNofityTick会被设置成true,继而每帧会执行TickNode

TickNode中,首先获取到成员变量结构体,然后取出上一次执行结束的时间,用当前时间减去它,如果超过了cd,才执行检测。执行检测的方法是调用OwnerComp.RequestExecution(this),它会调用decorator的check方法。

Buy Me A Coffee / 捐一杯咖啡的钱
分享这篇文章~
0%
//