目录:
一、创建C++类
二、关键函数
三、实际代码举例
一、创建C++类
Decorator应继承UBTDecorator,Task应继承UBTTaskNode。如果希望使用黑板键,可以继承自相应的有_BlackboardBase后缀的类,这个类会提供一个黑板键成员变量,当然也可以不继承这个_BlackboardBase,自己写
(以下代码中中文仅代表自定义名称占位符,实际代码中请不要出现中文)
protected:
/** blackboard key selector */
UPROPERTY(EditAnywhere, Category=Blackboard)
struct FBlackboardKeySelector 黑板键成员变量名;
然后在构造函数里加上黑板键的类型筛选,例如如果只希望可以选择float类型的黑板键,则:
BlackboardKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(你的类名, 黑板键成员变量名));
并且
/*声明:*/
virtual void InitializeFromAsset(UBehaviorTree& Asset) override;
/*实现:*/
Super::InitializeFromAsset(Asset);
UBlackboardData* BBAsset = GetBlackboardAsset();
if (ensure(BBAsset))
{
黑板键成员变量名.ResolveSelectedKey(*BBAsset);
}
如果希望自定义节点名,如
就在构造函数里
NodeName = "Wait";
如果希望节点有详细的描述,如
就重写
virtual FString GetStaticDescription() const override;
返回值就是这个描述。如果希望调试时能实时得知运行情况,如:
就需要重写
virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const override;
输出值为参数Values,如
Values.Add(想要输出的FString);
二、关键函数
Task开始执行函数为:
/** starts this task, should return Succeeded, Failed or InProgress
* (use FinishLatentTask() when returning InProgress)
* this function should be considered as const (don't modify state of object) if node is not instanced! */
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
其可用的返回值有三种
EBTNodeResult::Failed
EBTNodeResult::Succeeded
EBTNodeResult::InProgress
前两种自然就是立刻返回当前节点执行成功与否,最后一种是表示节点虽然已经开始执行,但暂时还没执行结束,当希望结束执行InProgress的节点(以成功执行举例),就需要调用
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
一般在TickTask中调用,也就是每个Tick都去检查是否执行完成。
TickTask函数原型:
/** ticks this task
* this function should be considered as const (don't modify state of object) if node is not instanced! */
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds);
(注意,只有当设置bNotifyTick = true时TickTask才会执行,同理还有bNotifyActivation 于OnNodeActivation、bNotifyDeactivation 于OnNodeDeactivation)
Decorator的判断函数为
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
重写这个函数,返回值则是能否执行子节点。
当有黑板键的值发生变化时,会调用
virtual EBlackboardNotificationResult OnBlackboardKeyValueChange(const UBlackboardComponent& Blackboard, FBlackboard::FKey ChangedKeyID) override;
如果希望控制Decorator父节点的哪个子节点被执行,可以调用:
GetParentNode()->SetChildOverride(SearchData, ChildIndex);
其中SearchData是函数OnNodeActivation或OnNodeDeactivation的参数,ChildIndex是签名为UBTAuxiliaryNode::ChildIndex,UBTDecorator就是继承UBTAuxiliaryNode的,这个成员变量的意义是正在执行的子节点索引。这类功能函数不能一一列举,建议自行查看源码。
额外一句,GetParentNode()的返回值是UBTCompositeNode,而任意节点的父节点都是一个CompositeNode,因为叶子节点(行为树中只有一种也就是TaskNode)是没有子节点的。
Task和Decorator还提供了
virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override;
virtual uint16 GetInstanceMemorySize() const override;
两个函数,个人理解是为了方便此节点在堆上构造数据,有点像C语言先malloc然后再init,也即C++中new一个对象的过程,先分配,再构造 。
【修改:Node Memory设计理念:BTTask NodeMemory??】
其中GetInstanceMemorySize返回数据大小,一般用法为
uint16 你的类名::GetInstanceMemorySize() const
{
return sizeof(自定义的结构体名);
}
InitializeMemory则是初始化,NodeMemory就是分配好的GetInstanceMemorySize()长度的内存指针,InitType有两种,
Initialize, // first time initialization
RestoreSubtree, // loading saved data on reentering subtree
分别是 第一次初始化,以及 已经进入过而且已经离开了,现在又一次进入时。
三、实际代码举例
理论讲的够多了,接下来实际看代码。我选的例子是UBTDecorator_CoolDown,作用大家应该都清楚,让节点有冷却时间,也就是执行过一个节点,特定时间内不会再次执行。
.h
//这个结构体需要在堆上构造的,可以稍后在后续代码中得知,
//无论是GetInstanceMemorySize返回了这个结构体的size,
//或是InitializeMemory、CalculateRawConditionValue、DescribeRuntimeValues这三个函数都
//将参数uint8* NodeMemory给
//FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);
//(可以理解为Cast)
//都可以证明这一点
struct FBTCooldownDecoratorMemory
{
//上一次执行时的时间戳
//由这个变量就可以推导出CoolDown的判断实现为
//判断当前世界时间戳与此值差值是否大于期望
float LastUseTimestamp;
//一个flag,判断是否已经渡过冷却时间
//已渡过冷却时间后就不计算是否已渡过
//节省性能
//同时确保第一次执行不会失败
uint8 bRequestedRestart : 1;
};
/**
* Cooldown decorator node.
* A decorator node that bases its condition on whether a cooldown timer has expired.
*/
UCLASS(HideCategories=(Condition))
class AIMODULE_API UBTDecorator_Cooldown : public UBTDecorator
{
GENERATED_UCLASS_BODY()
/** max allowed time for execution of underlying node */
//暴露给行为树,以可以在Detail Panel中设置冷却时间
UPROPERTY(Category=Decorator, EditAnywhere)
float CoolDownTime;
//~ Begin UObject Interface
virtual void PostLoad() override;
//~ End UObject Interface
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
virtual void InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const override;
virtual uint16 GetInstanceMemorySize() const override;
virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const override;
virtual FString GetStaticDescription() const override;
#if WITH_EDITOR
virtual FName GetNodeIconName() const override;
#endif // WITH_EDITOR
protected:
virtual void OnNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type NodeResult) override;
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
.cpp
UBTDecorator_Cooldown::UBTDecorator_Cooldown(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
NodeName = "Cooldown";
CoolDownTime = 5.0f;
// aborting child nodes doesn't makes sense, cooldown starts after leaving this branch
bAllowAbortChildNodes = false;
bNotifyTick = false;
bNotifyDeactivation = true;
}
void UBTDecorator_Cooldown::PostLoad()
{
Super::PostLoad();
//这一句是,只要Abort方式不是None,那就需要Tick
//因为只有在有可能Abort掉子节点或同级低优先节点的情况下才需要逐tick检查
bNotifyTick = (FlowAbortMode != EBTFlowAbortMode::None);
}
bool UBTDecorator_Cooldown::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);
const float TimePassed = (OwnerComp.GetWorld()->GetTimeSeconds() - DecoratorMemory->LastUseTimestamp);
return TimePassed >= CoolDownTime;
}
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;
}
void UBTDecorator_Cooldown::OnNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type NodeResult)
{
FBTCooldownDecoratorMemory* DecoratorMemory = GetNodeMemory<FBTCooldownDecoratorMemory>(SearchData);
DecoratorMemory->LastUseTimestamp = SearchData.OwnerComp.GetWorld()->GetTimeSeconds();
DecoratorMemory->bRequestedRestart = false;
}
void UBTDecorator_Cooldown::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);
if (!DecoratorMemory->bRequestedRestart)
{
const float TimePassed = (OwnerComp.GetWorld()->GetTimeSeconds() - DecoratorMemory->LastUseTimestamp);
if (TimePassed >= CoolDownTime)
{
DecoratorMemory->bRequestedRestart = true;
OwnerComp.RequestExecution(this);
}
}
}
FString UBTDecorator_Cooldown::GetStaticDescription() const
{
// basic info: result after time
return FString::Printf(TEXT("%s: lock for %.1fs after execution and return %s"), *Super::GetStaticDescription(),
CoolDownTime, *UBehaviorTreeTypes::DescribeNodeResult(EBTNodeResult::Failed));
}
void UBTDecorator_Cooldown::DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const
{
Super::DescribeRuntimeValues(OwnerComp, NodeMemory, Verbosity, Values);
FBTCooldownDecoratorMemory* DecoratorMemory = CastInstanceNodeMemory<FBTCooldownDecoratorMemory>(NodeMemory);
const float TimePassed = OwnerComp.GetWorld()->GetTimeSeconds() - DecoratorMemory->LastUseTimestamp;
if (TimePassed < CoolDownTime)
{
Values.Add(FString::Printf(TEXT("%s in %ss"),
(FlowAbortMode == EBTFlowAbortMode::None) ? TEXT("unlock") : TEXT("restart"),
*FString::SanitizeFloat(CoolDownTime - TimePassed)));
}
}
uint16 UBTDecorator_Cooldown::GetInstanceMemorySize() const
{
return sizeof(FBTCooldownDecoratorMemory);
}
#if WITH_EDITOR
FName UBTDecorator_Cooldown::GetNodeIconName() const
{
return FName("BTEditor.Graph.BTNode.Decorator.Cooldown.Icon");
}
#endif // WITH_EDITOR
Task我推荐的是UBTTask_Wait,需要了解黑板键的可以去UBTTask_WaitBlackboardTime
可以自行去阅读源码。