行为树介绍
在制作游戏时,我们或多或少会加一些AI在里面,来充实我们的游戏内容。我们的AI受AIController控制,而行为树就作为AIController的大脑存在,指导着AI做事。虚幻引擎中的行为树组件BehaviorTreeComponent继承自BrainComponent,AIController就持有BrainComponent,当你写好行为树时,通过AIController就可以运行一颗行为树来让你的AI动起来了。
你可以在对应的AIController中通过RunBehaviorTree来运行你写好的行为树。
行为树的节点概念
行为树是一个树状结构,它的执行顺序是从根节点开始,从上到下,从左至右依次执行各个节点,大家可以随意连一棵行为树,在这棵树内每个节点的右上角都会帮你打上序号代表这个节点的执行顺序。
行为树中的节点是有返回结果的,返回结果有四种Succeeded代表成功。Failed代表失败。InProgress代表运行中,我们如果一个节点是持续进行的就可以让他返回InProgress并在合适时机返回结果即可。Aborted,代表被打断,一个节点可以被自身或者左侧更高优先级的行为打断。
CompositeNode:复合节点,这类节点不支持你有过多的逻辑,它通常用于控制行为树运行哪些子节点,你可以在C++中自定义新的复合节点,但是无法通过蓝图来自定义一个新复合节点。
TaskNode:任务节点,AI的行为逻辑应该实现在这里,在编写Task时我们需要让其返回结果供父节点判断接下来应该做的行为,同时你可能需要对其打断进行处理即当这个节点被打断时应该去做什么逻辑。蓝图和C++都可以自定义新的Task。
Decorator:装饰器节点,这个节点需要挂载到CompositeNode或者TaskNode上。这是一个辅助节点,对被挂载节点做一些辅助性的任务 。它的主要作用是判断当前被挂载节点是否可以执行,Decorator在执行到被挂载的节点前执行。同时它也可以条件符合时打断低等级节点执行自己的节点或者条件不符合时打断自己的节点返回Aborted,关于Decorator打断的逻辑我们在后面部分再讲,这里先让大家知道概念。蓝图和C++都可以自定义新的Decorator。
Service:服务节点,这个节点同样需要挂在CompositeNode或者TaskNode上,这也是一个辅助性节点。它可以与当前CompositeNode或者TaskNode节点及其子节点并行执行,Service通过拟定的频率执行,通常用于检查更新黑板。蓝图和C++都可以自定义新的Service。
黑板介绍
黑板可以用于存储和读写一个AI需要的数据,它与行为树绑定使用。我们现在知道Decorator具有打断功能,但如果行为树每帧都去检查每个Decorator的话肯定会很影响我们的整体性能。虚幻引擎就选择了事件驱动的方式来做这件事,当黑板中有值被改变时行为树会去检查Decorator来触发打断。我们不必把所有的数据都放在黑板中,通常只需要把一些关键的数据放入即可,比如AI状态等。
行为树配置方法
展示一张示例图,大家可以先不用再以这些节点的具体作用是什么,只需要关注到配置方法就可以,Service和Decorator是需要挂在其他节点上的,而复合节点和Task就可以单独存在,大家也需要关注一下图片中行为树的执行顺序。
节点配置方法
CompositeNode
上图中出现了两种复合节点,这两种是最常用的复合节点,它们不执行具体的行为,只决定接下来该走哪条子节点分支。
Sequence:依次执行子节点,当一个子节点返回Succeeded时会继续执行下一个子节点直到返回非Succeeded或者执行完所有子节点。若全部节点均Succeeded此节点也返回Succeeded,否则返回Failed。
Selector:依次执行子节点,当一个子节点返回Failed时会继续执行下一个子节点直到返回非Failed或者执行完所有子节点。若全部节点均Failed此节点也返回Failed,否则返回Succeeded。与Sequence相反。
UE官方也提供了第三种复合节点Simple Parallel,它可以并行执行左右两个子节点。
TaskNode
配置Task节点根据你的游戏逻辑来把需要的节点连接到行为树上即可。这个这个节点配置时需要注意黑板键的选择和自身所需参数。
Task节点通常会向行为树开放出一些参数可供配置,这些参数会影响Task内部逻辑的执行。
你会发现有些节点没有给你配置黑板键,而有的则需要,这个也是根据节点具体逻辑来决定。
拿UE自带的Wait节点和MoveTo节点举例子,Wait不需要黑板键,而MoveTo则需要你去指定一个FVector或者Actor类型的黑板键,这对应了MoveTo的两种模式,一种是移动到某一点另一种是移动到某个Actor上,同时他们都有可以配置的参数例如Wait的时间以及MoveTo是否要考虑胶囊体半径等。
Decorator
Decorator会在执行被挂载节点前进行一次检查,在你配置好的一个Task或复合节点上可以配置多个Task,他们会从上到下依次进行检查。使用时需要配置其参数,黑板键以及观察器周期,某些函数只有处于观察器周期时才会生效。
直接上图可以看到能够配置的观察器周期有四种
Self:周期为自身Task节点起始结束
LowerPriority:周期为所有右侧节点低优先级Task节点起始至所有右侧节点低优先级Task结束
Both:周期为自身Task节点起始至所有右侧节点低优先级Task结束。
Decorator的Tick函数只能在观察器周期触发。
看这个参数名字为观察器中止我们就知道,这个参数配置后不止决定了观察周期,同时也决定了中断时机。
我们选择self或者both,当执行自身Task或者子节点时发现Decorator判定不通过则会打断当前节点。
我们选择LowerPriority或者both,当执行低优先级Task时发现自身Decorator判定通过则会打断当前节点,执行自身Task。
所以在编写自定义Task节点时要做好其打断处理,防止这个Task突然中断。
那么打断是怎么实现的呢。其实就是重新执行了一下这个节点
BehaviorComp->RequestExecution(this);
这里推荐程序同学去看一下UBTDecorator_TimeLimit这个节点,当我们中断自身之后也是重新执行自己的节点,但是会先判断,当判断发现是第二次执行就会返回失败。
Service
前面介绍到Service可以与被挂载节点一起执行,它的执行由频率控制,那么配置的时候就可以对这个频率进行控制,你可以填写Tick的时间间隔以及间隔的上下浮动值,同时你也可以配置这个Service是立即执行还是过一个间隔执行。大家可以参考下图,这是UE官方提供的RunEQS。
节点自定义编写方法
在UE的开发过程中,通过代码加蓝图的模式才是最好的开发模式,大家要在开发的过程中不要只使用代码,记得和策划配合使用好蓝图,当然程序同学也可以像UE一样把自定义的模块开放成蓝图可编辑供策划同学使用。
回归正文。因为UE对行为树的单例处理以及代码能够更细致自定义节点的原因,我们先讲蓝图的编写方法,然后再讲代码编写节点的方法。
蓝图自定义节点方法
TaskNode
我们需要继承BTTask_BluePrintBase,然后实现三个关键的函数就可以了。
那么问题来了,为什么同名函数后面会加一个AI呢,但是你在实际的使用中或许不会发现他们有什么区别,这里我们直接看代码吧。
EBTNodeResult::Type UBTTask_BlueprintBase::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// fail when task doesn't react to execution (start or tick)
CurrentCallResult = (ReceiveExecuteImplementations != 0 || ReceiveTickImplementations != 0) ? EBTNodeResult::InProgress : EBTNodeResult::Failed;
bIsAborting = false;
if (ReceiveExecuteImplementations != FBTNodeBPImplementationHelper::NoImplementation)
{
bStoreFinishResult = true;
if (AIOwner != nullptr && (ReceiveExecuteImplementations & FBTNodeBPImplementationHelper::AISpecific))
{
ReceiveExecuteAI(AIOwner, AIOwner->GetPawn());
}
else if (ReceiveExecuteImplementations & FBTNodeBPImplementationHelper::Generic)
{
ReceiveExecute(ActorOwner);
}
bStoreFinishResult = false;
}
return CurrentCallResult;
}
ReceiveExecuteImplementations变量表示我们是否在蓝图重写了ReceiveExecute或ReceiveExecuteAI函数。如果重写就会选择其一执行,当能够获取AIController时就会优先执行AI结尾函数,否则执行另一种。
我们可以看到当你重写了Excute或者Tick函数后,代码返回的是InProgress。所以大家一定要在蓝图中结束我们的Task,如果没有结束的话行为树会一直等待这个节点的结束。当然如果你在蓝图重写了Abort函数也要记得结束。
Decorator
想要在蓝图编写Decorator也需要继承BTDecorator_BluePrintBase,我们创建之后会发现蓝图里有12个函数可以实现,他们有着不同的触发时机,我们第一次去了解这几个函数时肯定会一脸懵逼,那么我来给大家介绍下吧。
首先是检查函数,当我们需要检查时总会触发这个,还记得我前面提到了Decorator检查是事件驱动的吗,大家可以在蓝图里print或者log一些东西,肯定会疑惑为什么这个Check是每次Tick都触发呢?难道是讲错了吗。其实UE这里的设计就是利用了Tick,通过事件触发检查是需要利用到黑板的,但是这个UE怎么能确定你这个蓝图检查函数用的是黑板键检查呢,所以它选择了在观察的生命周期内Tick触发检查。
void UBTDecorator_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
{
ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
}
else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
{
ReceiveTick(ActorOwner, DeltaSeconds);
}
// possible this got ticked due to the decorator being configured as an observer
if (GetNeedsTickForConditionChecking())
{
RequestAbort(OwnerComp, EvaluateAbortType(OwnerComp));
}
}
UBTDecorator_BlueprintBase::EAbortType UBTDecorator_BlueprintBase::EvaluateAbortType(UBehaviorTreeComponent& OwnerComp) const
{
if (PerformConditionCheckImplementations == 0)
{
return EAbortType::Unknown;
}
if (FlowAbortMode == EBTFlowAbortMode::None)
{
return EAbortType::NoAbort;
}
const bool bIsOnActiveBranch = OwnerComp.IsExecutingBranch(GetMyNode(), GetChildIndex());
EAbortType AbortType = EAbortType::NoAbort;
if (bIsOnActiveBranch)
{
if ((FlowAbortMode == EBTFlowAbortMode::Self || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) == IsInversed())
{
AbortType = EAbortType::DeactivateBranch;
}
}
else
{
if ((FlowAbortMode == EBTFlowAbortMode::LowerPriority || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) != IsInversed())
{
AbortType = EAbortType::ActivateBranch;
}
}
return AbortType;
}
当然如果你使用了一个黑板键选择器的话这个蓝图也是可以触发事件检测的,不过在Tick种检查的操作依旧存在。所以大家想要实现通过黑板键检查并且需要在观察周期打断功能的话,尽量还是通过C++代码来实现来保证行为树的性能。
这两个函数在检查通过的情况下,自身Task节点起始结束时触发。
当我们开启观察之后,这三个函数分别对会在观察器周期起始结束以及每帧时调用。
Service
可以看到这里有四个可以实现的函数。
Activation函数会在被挂载节点开始时调用,Deactivation函数会在被挂载节点结束时调用,Tick函数在被挂载节点以及子节点运行时调用。当我们将Service挂载到一个复合节点时,会在复合节点启动时运行SearchStart函数。
C++自定义节点方法
如果想做好我们的游戏,通过代码开发是必不可少的环节,在UE的行为树代码开发中,我们可以做到比蓝图更细致的操作以及更好的性能。策划同学如果没有代码权限的话可以先不用向下看了,UE为蓝图提供的BlueBase节点已经处理了大部分细节了。
下面我会向大家简单介绍代码编写节点时的需要完成的主要任务,因为更具体的使用细节以及各个函数的功能可以自行测试。
CompositeNode
在编写CompositeNode时我们通常用它来决定我会执行哪些子节点,会以什么样的顺序去执行。我们需要关注GetNextChildHandler函数的实现,这个函数你可以拿到上一个执行的子节点的下标以及上次执行的结果,你可以根据这两个参数来决定接下来执行哪个节点。如果你想执行的下一个子节点下标为n则返回n,否则返回BTSpecialChild::ReturnToParent。
我们来看一下官方完成的Sequece。
int32 UBTComposite_Sequence::GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const
{
// failure = quit
int32 NextChildIdx = BTSpecialChild::ReturnToParent;
if (PrevChild == BTSpecialChild::NotInitialized)
{
// newly activated: start from first
NextChildIdx = 0;
}
else if (LastResult == EBTNodeResult::Succeeded && (PrevChild + 1) < GetChildrenNum())
{
// success = choose next child
NextChildIdx = PrevChild + 1;
}
return NextChildIdx;
}
bool UBTComposite_Sequence::CanAbortLowerPriority() const
{
// don't allow aborting lower priorities, as it breaks sequence order and doesn't makes sense
return false;
}
可以发现它之后在子节点返回Succeeded时再执行下一个节点否则就会返回父节点。
这段代码我们会发下还有一个CanAbortLowerPriority函数,设置了这个之后,挂载到这个复合节点上的Decorator就不能把观察器的周期设置为LowerPriority。Sequece节点这个做的原因就是为了保持Sequece的连续性。同样的,CanAbortSelf函数的作用也是类似的。
TaskNode
EBTNodeResult::Type ExecuteTask:节点的执行函数,在这个函数中你需要返回执行结果,如果你希望编写的节点是一个不立刻返回结果而是持续执行,那么你需要让这个函数返回InProgress来表示这个函数持续进行,当你返回InProgress后,行为树会监听这个函数的结束。然后我们就可以做一些持续性的事件,当事件结束时通过FinishLatentTask来结束监听告诉行为树这个节点已经结束,这里我们可以看下UE的MoveTo节点。
UE中的MoveTo节点中并没有操控AI走到目的地,而是启动一个AITask_MoveTo任务来完成移动,当执行节点时,将你在行为树中填写的参数传递到这个任务中,由它来完成移动。具体的移动涉及到其他模块内容,我们这里不做讲解,只需知道与行为树相关的内容即可。
Task节点中可以启动UGameplayTask任务来完成具体的行为,同时你的Task节点会对其进行监听,当这个UGameplayTask任务结束时会触发OnGameplayTaskDeactivated函数。
我们继续看UE中的MoveTo节点,当AITask_MoveTo任务结束时Task节点会调用FinishLatentTask函数来告诉行为树这个节点的执行结果。FinishLatentTask内需要你传入节点对应的UBehaviorTreeComponent*以及结果。
void UBTTask_MoveTo::OnGameplayTaskDeactivated(UGameplayTask& Task)
{
// AI move task finished
UAITask_MoveTo* MoveTask = Cast<UAITask_MoveTo>(&Task);
if (MoveTask && MoveTask->GetAIController() && MoveTask->GetState() != EGameplayTaskState::Paused)
{
UBehaviorTreeComponent* BehaviorComp = GetBTComponentForTask(Task);
if (BehaviorComp)
{
uint8* RawMemory = BehaviorComp->GetNodeMemory(this, BehaviorComp->FindInstanceContainingNode(this));
const FBTMoveToTaskMemory* MyMemory = CastInstanceNodeMemory<FBTMoveToTaskMemory>(RawMemory);
if (MyMemory && MyMemory->bObserverCanFinishTask && (MoveTask == MyMemory->Task))
{
const bool bSuccess = MoveTask->WasMoveSuccessful();
FinishLatentTask(*BehaviorComp, bSuccess ? EBTNodeResult::Succeeded : EBTNodeResult::Failed);
}
}
}
}
void OnTaskFinished:节点结束时触发函数。
EBTNodeResult::Type AbortTask:节点被打断时触发函数,如果你的行为树中有节点可以打断的话请处理好这个函数,例如在这个函数里中断未完成的技能或者停止Move等操作。
void TickTask:节点执行时每一帧执行的行为。
void SetNextTickTime:设置你下一次Tick的时间间隔。
在写Task时有一个重要的问题,那就是串数据了,(其实所有节点都有这个问题,只是一般大家把数据放Task里)。简单来说就是你有ABC三个AI执行同一个行为树,某一个节点流程是存储自身Name然后输出,可能你会发现有的有个AI输出了其他AI的Name。
根本原因是因为我们的一颗行为树只有一个实例,我们所有用这颗行为树的AI都是在使用同一个实例,所有他们执行的节点也都是同一个实例。
为了解决这个办法可以有两种办法。
1.如果你想快速解决问题,可以直接在节点构造函数中将bCreateNodeInstance 设置为true即可。
bCreateNodeInstance = true;
2.正规的解决办法是把数据存储在结构体上,然后实现GetInstanceMemorySize函数即可,实现后你的数据会被存储在行为树实例的一段连续内存上,你的NodeMemory会变成指向这个结构体具体位置的指针。你还可以实现InitializeMemory函数和CleanupMemory函数来初始化以及清理这些数据。
我们还是看下MoveTo节点。
struct FBTMoveToTaskMemory
{
/** Move request ID */
FAIRequestID MoveRequestID;
FDelegateHandle BBObserverDelegateHandle;
FVector PreviousGoalLocation;
TWeakObjectPtr<UAITask_MoveTo> Task;
uint8 bObserverCanFinishTask : 1;
};
uint16 UBTTask_MoveTo::GetInstanceMemorySize() const
{
return sizeof(FBTMoveToTaskMemory);
}
void UBTTask_MoveTo::InitializeMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryInit::Type InitType) const
{
InitializeNodeMemory<FBTMoveToTaskMemory>(NodeMemory, InitType);
}
void UBTTask_MoveTo::CleanupMemory(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTMemoryClear::Type CleanupType) const
{
CleanupNodeMemory<FBTMoveToTaskMemory>(NodeMemory, CleanupType);
}
具体数据存储好之后,怎么才能使用这些数据呢?
BTNodeResult::Type UBTTask_MoveTo::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type NodeResult = EBTNodeResult::InProgress;
FBTMoveToTaskMemory* MyMemory = CastInstanceNodeMemory<FBTMoveToTaskMemory>(NodeMemory);
MyMemory->PreviousGoalLocation = FAISystem::InvalidLocation;
MyMemory->MoveRequestID = FAIRequestID::InvalidRequest;
。。。。。。
}
我们大部分函数都有NodeMemory,把它转化为具体的数据类型就可以了。
Task节点可以使用黑板,UE帮我们实现好了一个UBTTask_BlackboardBase类来辅助操作,这里对黑板键进行了初始化的操作。
void UBTTask_BlackboardBase::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
UBlackboardData* BBAsset = GetBlackboardAsset();
if (BBAsset)
{
BlackboardKey.ResolveSelectedKey(*BBAsset);
}
else
{
UE_LOG(LogBehaviorTree, Warning, TEXT("Can't initialize task: %s, make sure that behavior tree specifies blackboard asset!"), *GetName());
}
}
这样我们只需将黑板键转化为具体类型就可以使用了。设置黑板键操作类似。
MyBlackboard->GetValue<UBlackboardKeyType_Vector>(BlackboardKey.GetSelectedKeyID()
Decorator
bool CalculateRawConditionValue:Decorator节点用于判断的节点,只有节点判断通过后才会只需其他函数以及被挂载节点逻辑。
使用其他函数时我们需要先在构造函数中进行初始化,请加上这个宏。
INIT_DECORATOR_NODE_NOTIFY_FLAGS();
void OnNodeActivation:判定通过后执行。
void OnNodeDeactivation:当被挂载节点及其子节点执行完毕返回父节点时执行。
如果我们设置了观察器的周期就会执行下面三个函数,这三个函数来自UBTAuxiliaryNode类而非UBTDecorator类
void OnBecomeRelevant:判定通过后如果我们开启了观察器则会执行。
void OnCeaseRelevant:当观察周期结束时执行。
void TickNode:观察周期每帧执行。
上文讲到观察器中断时有两种办法,分别是Tick中断以及事件中断。我们分别介绍一下这两种方法
1.正确来讲第一种检查方法可以是多种多样的,虽然大都是通过Tick来完成,但不是每次Tick执行检查的。我们来看下这两个官方的节点。
在UBTDecorator_TimeLimit节点构造时就设置了下一次Tick的时间,并且下一次Tick设置不再Tick并且重新执行此节点来做到自身打断。这个节点并没有每帧都进行中断检查
void UBTDecorator_TimeLimit::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::OnBecomeRelevant(OwnerComp, NodeMemory);
SetNextTickTime(NodeMemory, TimeLimit);
}
void UBTDecorator_TimeLimit::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, const float DeltaSeconds)
{
ensureMsgf(DeltaSeconds >= TimeLimit || FMath::IsNearlyEqual(DeltaSeconds, TimeLimit, UE_KINDA_SMALL_NUMBER),
TEXT("Using SetNextTickTime in OnBecomeRelevant should guarantee that we are only getting ticked when the time limit is finished. DT=%f, TimeLimit=%f"),
DeltaSeconds,
TimeLimit);
// Mark this decorator instance as Elapsed for calls to CalculateRawConditionValue
reinterpret_cast<FBTimeLimitMemory*>(NodeMemory)->bElapsed = true;
// Set our next tick time to large value so we don't get ticked again in case the decorator
// is still active after requesting execution (e.g. latent abort)
SetNextTickTime(NodeMemory, FLT_MAX);
OwnerComp.RequestExecution(this);
}
bool UBTDecorator_TimeLimit::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
return !reinterpret_cast<FBTimeLimitMemory*>(NodeMemory)->bElapsed;
}
接下来我们再看下UBTDecorator_BlueprintBase节点。可以发现它是每帧进行中断检查。
void UBTDecorator_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
{
ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
}
else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
{
ReceiveTick(ActorOwner, DeltaSeconds);
}
// possible this got ticked due to the decorator being configured as an observer
if (GetNeedsTickForConditionChecking())
{
RequestAbort(OwnerComp, EvaluateAbortType(OwnerComp));
}
}
UBTDecorator_BlueprintBase::EAbortType UBTDecorator_BlueprintBase::EvaluateAbortType(UBehaviorTreeComponent& OwnerComp) const
{
if (PerformConditionCheckImplementations == 0)
{
return EAbortType::Unknown;
}
if (FlowAbortMode == EBTFlowAbortMode::None)
{
return EAbortType::NoAbort;
}
const bool bIsOnActiveBranch = OwnerComp.IsExecutingBranch(GetMyNode(), GetChildIndex());
EAbortType AbortType = EAbortType::NoAbort;
if (bIsOnActiveBranch)
{
if ((FlowAbortMode == EBTFlowAbortMode::Self || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) == IsInversed())
{
AbortType = EAbortType::DeactivateBranch;
}
}
else
{
if ((FlowAbortMode == EBTFlowAbortMode::LowerPriority || FlowAbortMode == EBTFlowAbortMode::Both) && CalculateRawConditionValue(OwnerComp, /*NodeMemory*/nullptr) != IsInversed())
{
AbortType = EAbortType::ActivateBranch;
}
}
return AbortType;
}
这样你应该明白怎么通过Tick来完成中断了,只需要设置好Tick和CalculateRawConditionValue的逻辑就可以了。
2.如果你想要优化你的节点,可以选择第二种通过黑板键值变化进行通知的方法完成。UE提供的UBTDecorator_BlackboardBase类就帮你完成了这一步,你继承它就可以了。
可以看到不仅完成了黑板键的初始化还帮你把事件进行了注册,真的很方便。
void UBTDecorator_BlackboardBase::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
UBlackboardData* BBAsset = GetBlackboardAsset();
if (BBAsset)
{
BlackboardKey.ResolveSelectedKey(*BBAsset);
}
else
{
UE_LOG(LogBehaviorTree, Warning, TEXT("Can't initialize %s due to missing blackboard data."), *GetName());
BlackboardKey.InvalidateResolvedKey();
}
}
void UBTDecorator_BlackboardBase::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
auto KeyID = BlackboardKey.GetSelectedKeyID();
BlackboardComp->RegisterObserver(KeyID, this, FOnBlackboardChangeNotification::CreateUObject(this, &UBTDecorator_BlackboardBase::OnBlackboardKeyValueChange));
}
}
void UBTDecorator_BlackboardBase::OnCeaseRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
BlackboardComp->UnregisterObserversFrom(this);
}
}
EBlackboardNotificationResult UBTDecorator_BlackboardBase::OnBlackboardKeyValueChange(const UBlackboardComponent& Blackboard, FBlackboard::FKey ChangedKeyID)
{
UBehaviorTreeComponent* BehaviorComp = (UBehaviorTreeComponent*)Blackboard.GetBrainComponent();
if (BehaviorComp == nullptr)
{
return EBlackboardNotificationResult::RemoveObserver;
}
if (BlackboardKey.GetSelectedKeyID() == ChangedKeyID)
{
BehaviorComp->RequestExecution(this);
}
return EBlackboardNotificationResult::ContinueObserving;
}
Service
这个节点通常是与被挂载节点同时执行所用。关注以下几点即可。
void OnBecomeRelevant:节点被激活时调用。
void OnCeaseRelevant:节点结束时调用。
TickNode:在被挂载节点执行时执行,可以设置Tick的频率。注意执行这个节点是一定要调用Super::TickNode,因为父类的Tick计算了调用的时间。
OnSearchStart:被挂载节点为复合节点时调用。
总结
现在你应该明白了行为树的简单使用以及编写方法了,当然行为树还有更底层的初始化,搜索,执行等内容,这些可以在对行为树熟悉后再做了解。
文章有点长,写着写着逻辑都乱了。如有错误欢迎指正。