疑问
选中一个Decorator节点的时候,Detail面板中有一个Observe aborts
,并有如下选项:
- None
- Self
- Lower Priority
- Both
现有疑问:
- Self与Both区别如何?目前猜测:Both会继续执行接下来的节点,但是直接执行其abort回调函数
- Abort在Sequence和Selector中表现如何?会继续执行接下来的逻辑吗?
- Decorator是否与Service一样,全局共用同一个实例?如果是,如何做数据初始化?
Sequence
在Sequence中,一旦任务被abort,相当于运行失败,不会再运行下一个节点。
当decorator修饰的是Sequence下的任务节点时,你会发现只能够选择None
和Self
。
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 | uint16 UBTDecorator_Cooldown::GetInstanceMemorySize() const |
讲道理行为树执行这个方法之后,就知道要开辟一块多大的空间了,开辟空间之后,会把指针保存下来。
UBTNode
自带的很多函数,比如InitializeMemory
,OnNodeDeactivation
,TickNode
等,你会发现都有一个参数叫做NodeMemory
的,这个就是开辟的空间的指针(不准确地说,因为指针其实应该是int64的)。
使用如下语句可以把NodeMemory转换为对应的结构体,从结构体中可以读取出对应的成员变量:
1 | FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory); |
成员变量初始化
重写InitializeMemory
可以实现对成员变量结构体的初始化,作用相当于实例初始化的构造函数。比如在Cooldown的装饰器中,拿到成员变量结构体之后就可以进行初始化:
1 | void UBTDecorator_Cooldown::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const |
对于每一个节点,都会有其对应的一块内存。但是同一个节点在不同次进来,都是使用的同一块内存。
比如在一个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方法。