This post focuses on the basical concepts pf UE’ behavior tree and the difference between traditional behavior tree and UE’s behavior tree, which will not introduce to the basical usage of behavior tree.

If you are not familiar with the basic operation of behavior tree, take a look at: Behaviour Tree – User Guide

1. Composite nodes

In additional to traditional task nodes, there are two new concepts in UE’s behavior tree: the sub-nodes and the composite nodes.

Composite nodes brief

Composites are a form of flow control and determine how the child branches that are connected to them execute. Composites actually are traditional behavior tree solution, what makes it special in Unreal Engine is that how the composites deal with parallel. In traditional behavior tree, there maybe a parallel node, which allow every subtree of it executes in oreder. However, in UE’s behavior tree, it only supports “Simple Parallel”, which only allows a main task and a background task. The finish of this node depends on the finish mode of it. It will finish immediately then the main task finishes if the mode is “Immediately”, otherwise when the main task finished, the node continues executing until the background task finishes.

 

So, what about the traditional parallel? In fact, UE does that with sub nodes, which will be introduced soon.

All the composites are as followed:

 

CompositesDescription
SelectorExecutes branches from left-to-right and is typically used to select between subtrees. Selectors stop moving between subtrees when they find a subtree they successfully execute. For example, if the AI is successfully chasing the Player, it will stay in that branch until its execution is finished, then go up to the selector's parent composite to continue the decision flow.
SequencerExecutes branches from left-to-right and is more commonly used to execute a series of children in order. Unlike Selectors, the Sequence continues to execute its children until it reaches a node that fails. For example, if we had a Sequence to move to the Player, check if they are in range, then rotate and attack. If the check if they are in range portion failed, the rotate and attack actions would not be performed
Simple ParallelSimple Parallel has two "connections". The first one is the Main Task, and it can only be assigned a Task node (meaning no Composites). The second connection (the Background Branch) is the activity that's supposed to be executed while the main Task is still running. Depending on the properties, the Simple Parallel may finish as soon as the Main Task finishes, or wait for the Background Branch to finish as well.

 

Customizing a composite

customizing a composite requires for two classes, one is for editor and the other one is for runtime. Let’s customizing a simple composite, this composite will decline the first subtree and always execute the second subtree. It seems useless but enough for a costomizing tutorial. The three composites provided by Unreal Engine is enough for 90% requstment anyway.

First, create a class inherits from UBehaviorTreeGraphNode_Composite called  UBehaviorTreeGraphNode_UselessComposite in editor module.

If you have no idea about runtime modules and editor modules, see: Using Slate for UI development and editor(5): Modules

UCLASS()
class ADVANCEDALSEDITOR_API UBehaviorTreeGraphNode_UselessComposite : public UBehaviorTreeGraphNode_Composite
{
	GENERATED_UCLASS_BODY()
	
	virtual void AllocateDefaultPins() override;
	virtual void GetPinHoverText(const UEdGraphPin& Pin,
	FString& HoverTextOut) const override;
};

which is implemented as:

UBehaviorTreeGraphNode_UselessComposite::UBehaviorTreeGraphNode_UselessComposite(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}

void UBehaviorTreeGraphNode_UselessComposite::AllocateDefaultPins()
{
	CreatePin(EGPD_Input, UBehaviorTreeEditorTypes::PinCategory_MultipleNodes, TEXT("In"));

	CreatePin(EGPD_Output, UBehaviorTreeEditorTypes::PinCategory_SingleTask, TEXT("Task"));
	CreatePin(EGPD_Output, UBehaviorTreeEditorTypes::PinCategory_SingleNode, TEXT("Out"));
}

void UBehaviorTreeGraphNode_UselessComposite::GetPinHoverText(const UEdGraphPin& Pin, FString& HoverTextOut) const
{
	ensure(Pin.GetOwningNode() == this);
	if (Pin.Direction == EGPD_Output)
	{
		if (Pin.PinType.PinCategory == UBehaviorTreeEditorTypes::PinCategory_SingleTask)
		{
			HoverTextOut = NSLOCTEXT("EditorExtension", "PinHoverUselessMain","Main task the never execute").ToString();
		}
		else
		{
			HoverTextOut = NSLOCTEXT("EditorExtension", "PinHoverUeslessBackground","Sub tree that will be executed").ToString();
		}
	}
}

And then is the runtime class:

UCLASS(HideCategories=(Composite))
class AIMODULE_API UBTComposite_UselssParallel : public UBTCompositeNode
{
	GENERATED_UCLASS_BODY()

	/** how background tree should be handled when main task finishes execution */
	UPROPERTY(EditInstanceOnly, Category = Parallel)
	TEnumAsByte<EBTParallelMode::Type> FinishMode;

	/** handle child updates */
	virtual int32 GetNextChildHandler(FBehaviorTreeSearchData& SearchData, int32 PrevChild, EBTNodeResult::Type LastResult) const override;

	virtual void NotifyChildExecution(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, int32 ChildIdx, EBTNodeResult::Type& NodeResult) const override;
	virtual void NotifyNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type& NodeResult) const override;
	virtual bool CanNotifyDecoratorsOnDeactivation(FBehaviorTreeSearchData& SearchData, int32 ChildIdx, EBTNodeResult::Type& NodeResult) const override;
	virtual bool CanPushSubtree(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, int32 ChildIdx) const override;
	virtual void SetChildOverride(FBehaviorTreeSearchData& SearchData, int8 Index) const override;
	virtual uint16 GetInstanceMemorySize() const override;
	virtual FString GetStaticDescription() const override;
	virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray<FString>& Values) const override;

	/** helper for showing values of EBTParallelMode enum */
	static FString DescribeFinishMode(EBTParallelMode::Type Mode);

#if WITH_EDITOR
	virtual bool CanAbortLowerPriority() const override;
	virtual bool CanAbortSelf() const override;
	virtual FName GetNodeIconName() const override;
#endif // WITH_EDITOR
};

The implementation of it is to long to paste here, but it is mainly the same as simple parallel, differences most occurs in NotifyChildExecution and GetNextChildHandler, readers can implement this by refering to UBTComposite_SimpleParallel.

2. Sub nodes

Sub node is an important concepts of UE’s behavior tree, generally, there are two sub nodes: Decorator and Service:

 

SubnodeDescription
DecoratorAlso known as conditionals. These attach to another node and make decisions on whether or not a branch in the tree, or even a single node, can be executed.
ServiceThese attach to both Task and Composite nodes, and will execute at their defined frequency as long as their branch is being executed. These are often used to make checks and to update the Blackboard. These take the place of traditional Parallel nodes in other Behavior Tree systems.

 

Decorators brief

Decorator, also named as conditional, is used to attached to another node and check if a node or a subtree can be executed. We take Blackboard Based Condition node as an example.

If you have read the Post I, it maybe makes you remeber about what we have done before:

EBlackboardCompare::Type UBlackboardKeyType_Double::CompareValues(const UBlackboardComponent& OwnerComp,
	const uint8* MemoryBlock, const UBlackboardKeyType* OtherKeyOb, const uint8* OtherMemoryBlock) const
{
	const float MyValue = GetValue(this, MemoryBlock);
	const float OtherValue = GetValue((UBlackboardKeyType_Double*)OtherKeyOb, OtherMemoryBlock);

	return (FMath::Abs(MyValue - OtherValue) < KINDA_SMALL_NUMBER) ? EBlackboardCompare::Equal :
		(MyValue > OtherValue) ? EBlackboardCompare::Greater :
		EBlackboardCompare::Less;
}

bool UBlackboardKeyType_Double::TestArithmeticOperation(const UBlackboardComponent& OwnerComp, const uint8* MemoryBlock,
	EArithmeticKeyOperation::Type Op, int32 OtherIntValue, float OtherFloatValue) const
{
	const float Value = GetValue(this, MemoryBlock);
	switch (Op)
	{
	case EArithmeticKeyOperation::Equal:			return (Value == OtherFloatValue);
	case EArithmeticKeyOperation::NotEqual:			return (Value != OtherFloatValue);
	case EArithmeticKeyOperation::Less:				return (Value < OtherFloatValue);
	case EArithmeticKeyOperation::LessOrEqual:		return (Value <= OtherFloatValue);
	case EArithmeticKeyOperation::Greater:			return (Value > OtherFloatValue);
	case EArithmeticKeyOperation::GreaterOrEqual:	return (Value >= OtherFloatValue);
	default: break;
	}

	return false;
}

And yes, these methods are used here:

So, here is the question: why wouldn’t use a node which is used as a leaf node?

In the standard model for behaviour, conditionals are used as a leaf node which simply do nothing but return successful or fail.

Making conditionals Decorators rather than Tasks has a couple of significant advantages:

  • Conditional Decorators make the Behavior Tree UI more intuitive and easier to read.
  • Since all leaves are action Tasks, it is easier to see what actual actions are being ordered by the tree.

There is a useful tool in decorators, which is Observer Abort. one comon usage of startard parallel node is that when the condition change to false, the task can finish immediately.

For example, if you have a cat that performs a sequence, such as “Hiss” and “Pounce”, you may want to give up immediately if the mouse escapes into its mouse hole. With parallel nodes, you would have a child that checks if the mouse can be pounced on, and then another child that the sequence would perform. We instead handle this by having our conditional Decorators observe their values and abort when necessary. In this example, you would have a “Mouse Can Be Pounced On?” Decorator on the Sequence, with “Observer Aborts” set to “Self”.

A decorator with an observer will change its editor style: the border of the node will becode green.

But why we don’t use a start parallel node? That is because, the behavior tree here is event-driven, which means it tries to avoid doing unnecessary work every frame, instead, it constantly check if the condition has changed, which improve the performance of the behaviour tree. Event-driven is one of the talentest degisn in UE’s behaviour tree.

Customizing decorator node

Customizing decorator node can be done by both C++ and blueprint. Note that unlike task and service, the docorator has two execution chain: ExecutionStart->ExecutionEnd and ObserveActivated->ObserverDeactivated.

If you choose C++, the base class is BTDecorator, which has four virtural functions:

/** called when underlying node is activated
 * this function should be considered as const (don't modify state of object) if node is not instanced! 
 * bNotifyActivation must be set to true for this function to be called
 * Calling INIT_DECORATOR_NODE_NOTIFY_FLAGS in the constructor of the decorator will set this flag automatically */
virtual void OnNodeActivation(FBehaviorTreeSearchData& SearchData);

/** called when underlying node has finished
 * this function should be considered as const (don't modify state of object) if node is not instanced! 
 * bNotifyDeactivation must be set to true for this function to be called
 * Calling INIT_DECORATOR_NODE_NOTIFY_FLAGS in the constructor of the decorator will set this flag automatically */
virtual void OnNodeDeactivation(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type NodeResult);

/** called when underlying node was processed (deactivated or failed to activate)
 * this function should be considered as const (don't modify state of object) if node is not instanced! 
 * bNotifyProcessed must be set to true for this function to be called 
 * Calling INIT_DECORATOR_NODE_NOTIFY_FLAGS in the constructor of the decorator will set this flag automatically */
virtual void OnNodeProcessed(FBehaviorTreeSearchData& SearchData, EBTNodeResult::Type& NodeResult);

/** calculates raw, core value of decorator's condition. Should not include calling IsInversed */
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const;

The simplest node is that only implements CalculateRawConditionValue, for example, the UBTDecorator_CheckGameplayTagsOnActor:

bool UBTDecorator_CheckGameplayTagsOnActor::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	const UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
	if (BlackboardComp == NULL)
	{
		return false;
	}

	IGameplayTagAssetInterface* GameplayTagAssetInterface = Cast<IGameplayTagAssetInterface>(BlackboardComp->GetValue<UBlackboardKeyType_Object>(ActorToCheck.GetSelectedKeyID()));
	if (GameplayTagAssetInterface == NULL)
	{
		return false;
	}
	
	switch (TagsToMatch)
	{
		case EGameplayContainerMatchType::All:
			return GameplayTagAssetInterface->HasAllMatchingGameplayTags(GameplayTags);

		case EGameplayContainerMatchType::Any:
			return GameplayTagAssetInterface->HasAnyMatchingGameplayTags(GameplayTags);

		default:
		{
			UE_LOG(LogBehaviorTree, Warning, TEXT("Invalid value for TagsToMatch (EGameplayContainerMatchType) %d.  Should only be Any or All."), static_cast<int32>(TagsToMatch));
			return false;
		}
	}
}

Customizing by C++ is flexiable, but it is too low-level, we rarely use it, instread, customizing by blueprint is a much commonly used.

BTDecorator_BlueprintBase is a wrapped BPDecorator, methods exposed to the blueprint are:

/** tick function
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	void ReceiveTick(AActor* OwnerActor, float DeltaSeconds);

	/** called on execution of underlying node 
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	void ReceiveExecutionStart(AActor* OwnerActor);

	/** called when execution of underlying node is finished 
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	void ReceiveExecutionFinish(AActor* OwnerActor, enum EBTNodeResult::Type NodeResult);

	/** called when observer is activated (flow controller) 
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	void ReceiveObserverActivated(AActor* OwnerActor);

	/** called when observer is deactivated (flow controller) 
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	void ReceiveObserverDeactivated(AActor* OwnerActor);

	/** called when testing if underlying node can be executed, must call FinishConditionCheck
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent)
	bool PerformConditionCheck(AActor* OwnerActor);

	/** Alternative AI version of ReceiveTick
	 *	@see ReceiveTick for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	void ReceiveTickAI(AAIController* OwnerController, APawn* ControlledPawn, float DeltaSeconds);

	/** Alternative AI version of ReceiveExecutionStart
	 *	@see ReceiveExecutionStart for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	void ReceiveExecutionStartAI(AAIController* OwnerController, APawn* ControlledPawn);

	/** Alternative AI version of ReceiveExecutionFinish
	 *	@see ReceiveExecutionFinish for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	void ReceiveExecutionFinishAI(AAIController* OwnerController, APawn* ControlledPawn, enum EBTNodeResult::Type NodeResult);

	/** Alternative AI version of ReceiveObserverActivated
	 *	@see ReceiveObserverActivated for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	void ReceiveObserverActivatedAI(AAIController* OwnerController, APawn* ControlledPawn);

	/** Alternative AI version of ReceiveObserverDeactivated
	 *	@see ReceiveObserverDeactivated for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	void ReceiveObserverDeactivatedAI(AAIController* OwnerController, APawn* ControlledPawn);

	/** Alternative AI version of ReceiveConditionCheck
	 *	@see ReceiveConditionCheck for more details
	 *	@Note that if both generic and AI event versions are implemented only the more
	 *	suitable one will be called, meaning the AI version if called for AI, generic one otherwise */
	UFUNCTION(BlueprintImplementableEvent, Category = AI)
	bool PerformConditionCheckAI(AAIController* OwnerController, APawn* ControlledPawn);

3. Concurrent Behaviors

Standard behavior tree uses parallel node for concurrent behaviours, but in UE’ behavior, we use simple parallel and a sub node called services.

Simple parallel

Simple Parallel nodes have only two children: one which must be a single Task node (with optional Decorators), and the other of which can be a complete sub-tree. Think of the Simple Parallel node as “While doing A, do B as well.” For example, “While attacking the enemy, move toward the enemy.” A is a primary task, and B is a secondary or filler task while waiting for A to complete.

While there are some options as to how to handle the secondary task (Task B), the node is relatively simple in concept compared to traditional parallel nodes. Nonetheless, it supports much of the most common usage of parallel nodes. Simple Parallel nodes allow easy usage of our event-driven optimizations while full parallel nodes would be much more complex to optimize.

Services

Services are special nodes associated with any Composite node (Selector, Sequence, or Simple Parallel), which can register for callbacks every specified amount of seconds and perform updates of various sorts that need to occur periodically.

It works normal with not only composite nodes actually, it can be attached to a task node like:

We take Run EQS as an example, we will discuss about EQS in the following posts.

Service doesn’t need to be run every frame, it can have a time interval, ClampMin to 0.001, and even has a variable called RandomDeviation for random called interval:

void UBTService::ScheduleNextTick(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	const float NextTickTime = FMath::FRandRange(FMath::Max(0.0f, Interval - RandomDeviation), (Interval + RandomDeviation));
	SetNextTickTime(NodeMemory, NextTickTime);
}

For example, a service can be used to determine which enemy is the best choice for the AI Pawn to pursue while the Pawn continues to act normally in its Behavior Tree toward its current enemy.

Services are active as long as execution remains in the sub-tree of the Composite node where the service has been added.

Customizing Services

It can be done by C++ or blueprint, unlike decorators, I prefer C++ here, the core function is:

/** update next tick interval
	 * this function should be considered as const (don't modify state of object) if node is not instanced!
	 * bNotifyTick must be set to true for this function to be called 
	 * Calling INIT_SERVICE_NODE_NOTIFY_FLAGS in the constructor of the service will set this flag automatically */
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

	/** called when search enters underlying branch
	 * this function should be considered as const (don't modify state of object) if node is not instanced! 
	 * bNotifyOnSearch must be set to true for this function to be called  
	 * Calling INIT_SERVICE_NODE_NOTIFY_FLAGS in the constructor of the service will set this flag automatically */
virtual void OnSearchStart(FBehaviorTreeSearchData& SearchData);

While there are to many methods in BTService_BlueprintBase, which makes me confued.

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *