Hello friends after a long break! In this article, I will explain the Behavior Tree (BT), which is a decision-making structure used in many game AIs, and give code examples in the unreal engine. I hope it will be a useful article.
A Behavior Tree (BT) is a system for controlling NPC (Non-Player Character) decision-making in game AI. It allows NPCs to select and perform actions based on game state, organizing complex behaviors in a structured way. Unlike finite state machines, behavior trees are modular and adaptable, enabling layered combinations of tasks and conditions.
Core Components of a Behavior Tree
- Root Node: The starting point of the tree.
- Composite Nodes: Control nodes that determine the sequence and conditions for executing child nodes.
- Selector: Runs child nodes in sequence until one succeeds, ideal for fallback actions.
- Sequence: Executes child nodes in order until one fails, useful for strict sequences of actions.
- Decorator Nodes: Decorator Nodes in Behavior Trees are used to control when or how often certain tasks are executed, acting as conditions that must be met before an action can proceed. They’re essential for adding logic checks that make AI behaviors more responsive and situational.
Example Usage:- Blackboard Condition: Checks if a Blackboard value (like “IsPlayerInRange”) meets a specific condition before proceeding. For example, an attack task might have a “IsPlayerInRange” decorator, so the AI only attacks when close enough to the player.
- Cooldown: Sets a timer to limit how often a task can run. This is helpful for actions that should only happen periodically, like a defensive move or checking for threats every few seconds.
- Repeat: Repeats the attached task for a set number of times or indefinitely. Useful for looping behaviors, like repeatedly scanning for the player.
- Time Limit: Restricts the duration that a task can run. If the task exceeds this time, it stops, which is useful for timed actions like fleeing.
- Task Nodes: Task Nodes in Behavior Trees are where specific actions or behaviors for an AI character are defined. Each Task Node tells the AI to perform a single action, like moving, attacking, or playing an animation, and is only executed when all preceding conditions are met.
Example usage:- Move to Target: Directs the AI to move toward a specified location, often used for patrolling or chasing the player.
- Attack Target: Initiates an attack sequence when the target is within range. This might trigger animations or damage calculations.
- Play Animation: Plays a specified animation, like idle or celebration, adding character to the AI’s behavior.
- Patrol Points: Moves the AI between set waypoints, useful for creating guard or patrolling behaviors.
- Find Cover: Directs the AI to seek a nearby cover location, often triggered when the AI’s health is low or when under threat.
- Service Nodes: These are auxiliary nodes that provide continuous monitoring or recurring updates while attached to a Composite or Task node. Services run at regular intervals, allowing you to frequently check or update information without interrupting the flow of the tree.
- Example Usage: A Service node might continuously check if the player is within a certain range and update the Blackboard with this information, keeping the AI aware of changes in real time.
Let’s Dive in Unreal Engine with Examples
1. Root Node
The Root node is a foundational element of Unreal Engine’s Behavior Trees. It organizes and initiates the behavior logic for AI characters, ensuring that the tree functions cohesively from the top down. By using the Root node effectively, developers can create structured and complex behaviors that respond dynamically to in-game situations.
2. Composite Node
Selector node is a composite node that evaluates and executes its child nodes in order of priority. Its main function is to find and run the first successful child node, making it ideal for flexible decision-making when the AI has several potential actions to choose from and should attempt the first available one.
- Priority-Based Execution: The Selector node processes each child node sequentially, from left to right. It stops and executes the first child that succeeds, ignoring the remaining nodes.
- Failover System: If a child node fails, the Selector moves to the next one, continuing until a successful child is found or all nodes have been tried.
- If any child node succeeds, the Selector reports success to its parent.
- If all child nodes fail, the Selector reports failure.
- Dynamic Decision-Making: Selector nodes are well-suited for cases where the AI needs to try multiple actions until one is successful, such as deciding to patrol, engage in combat, or flee, with each behavior attempted in a specific order based on conditions or success criteria.
A Sequence node is a composite node that runs its child nodes in a set order, from left to right. It’s mainly used to set up a series of actions or checks that must all complete successfully for the entire sequence to be considered a success. Sequence nodes are ideal for chaining behaviors, as each action must finish successfully before the next one begins.
- Ordered Execution: The Sequence node processes each child node in turn, starting with the first and moving to the last. Each child must succeed for the Sequence to proceed to the next node.
- Success and Failure Handling:
- When a child node succeeds, the Sequence continues to the next child.
- If a child node fails, the Sequence immediately stops and returns failure to its parent node.
- A Sequence only returns success if all child nodes succeed.
- Sequential Logic: Sequence nodes are commonly applied to tasks that require specific steps in order, such as checking conditions and then performing actions based on those checks.
3. Decorator Node
A Decorator node is a control node that defines conditions for other nodes. Attached to composite or task nodes, decorators serve as “gatekeepers,” determining whether a particular node or sequence can run based on specific conditions. This enables flexible, condition-based AI behaviors.
Checks if an AI character has low health. If the character’s health is below a certain threshold, the decorator allows the Behavior Tree node it’s attached to (like a flee or heal action) to execute. This can help the AI make decisions based on its health status.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_IsLowHealth.generated.h"
/**
* Decorator that checks if the AI character's health is below a certain threshold.
*/
UCLASS()
class YOURGAME_API UBTDecorator_IsLowHealth : public UBTDecorator
{
GENERATED_BODY()
public:
// Constructor
UBTDecorator_IsLowHealth();
protected:
// The health threshold below which this decorator will succeed
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI")
float HealthThreshold = 30.0f;
// Blackboard key that stores the AI character's health
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI")
FBlackboardKeySelector HealthKey;
// Overrides the CalculateRawConditionValue to perform the low health check
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};
- HealthThreshold: Specifies the health value below which this decorator succeeds.
- HealthKey: References a Blackboard key where the AI’s current health is stored.
- CalculateRawConditionValue: This function is overridden to perform the check on the AI’s health.
#include "BTDecorator_IsLowHealth.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "GameFramework/Actor.h"
UBTDecorator_IsLowHealth::UBTDecorator_IsLowHealth()
{
NodeName = "Is Low Health";
}
bool UBTDecorator_IsLowHealth::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
Super::CalculateRawConditionValue(OwnerComp, NodeMemory);
// Get the Blackboard component
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (!BlackboardComp)
{
return false;
}
// Get the current health from the Blackboard
float CurrentHealth = BlackboardComp->GetValueAsFloat(HealthKey.SelectedKeyName);
// Check if the health is below the threshold
return CurrentHealth <= HealthThreshold;
}
The Blackboard component is accessed to obtain the current health value of the AI. The decorator reads the health value from the specified HealthKey on the Blackboard and then checks it against the defined HealthThreshold. If the AI’s health is equal to or below this threshold, the decorator returns true, enabling the execution of the behavior linked to it.
4. Task Node
Previously, we discussed that Task Nodes in Behavior Trees define specific actions or behaviors for an AI character. Each Task Node instructs the AI to carry out a single action, such as moving, attacking or finding random patrol point.
Now let’s write the BTTask code to find a random “patrol” point. You can create a custom BTTask node in Unreal Engine’s Behavior Tree that finds a random point within a specified radius and sets it as the next patrol destination. Here’s how to implement this in C++.
- Right-click in the Content Browser > Add New > C++ Class > BTTaskNode and name it BTTask_FindRandomPatrolPoint.
- This class will define the behavior for finding a random patrol point.
- BTTask_FindRandomPatrolPoint.h and define necessary components, such as references to Blackboard keys for storing the random location and radius of the area where the point will be found.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_FindRandomPatrolPoint.generated.h"
UCLASS()
class YOURGAME_API UBTTask_FindRandomPatrolPoint : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_FindRandomPatrolPoint();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
FVector GetRandomLocation(const FVector Origin, float Radius);
UPROPERTY(EditAnywhere, Category="Blackboard")
struct FBlackboardKeySelector PatrolLocationKey;
UPROPERTY(EditAnywhere, Category="Settings")
float SearchRadius = 1000.0f;
};
UBTTaskNode is a base class in Unreal Engine used for creating Task Nodes within the Behavior Tree framework. UBTTaskNode contains an ExecuteTask function that you override to define what the node does when executed. The ExecuteTask function returns an EBTNodeResult value (Succeeded, Failed, or InProgress) to indicate whether the task was completed, failed, or is still in progress.
The GetRandomLocation helper function is called, which takes the AI’s current location (Origin) and a SearchRadius (the radius in which the AI can patrol) to find a random navigable location within that area.
PatrolLocationKey is a BlackboardKeySelector, which is used to select a key on the Blackboard. This allows us to specify a Blackboard key in the Unreal Editor for storing the random patrol point location.
SearchRadius is a configurable parameter that defines the radius within which the random patrol point should be found.
- In BTTask_FindRandomPatrolPoint.cpp, implement the logic for finding a random location within a given radius and updating the Blackboard with this location.
#include "BTTask_FindRandomPatrolPoint.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "NavigationSystem.h"
UBTTask_FindRandomPatrolPoint::UBTTask_FindRandomPatrolPoint()
{
NodeName = "Find Random Patrol Point";
}
EBTNodeResult::Type UBTTask_FindRandomPatrolPoint::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* AIController = OwnerComp.GetAIOwner();
if (!AIController)
{
return EBTNodeResult::Failed;
}
APawn* ControlledPawn = AIController->GetPawn();
if (!ControlledPawn)
{
return EBTNodeResult::Failed;
}
// Get the current location of the AI character
FVector Origin = ControlledPawn->GetActorLocation();
// Generate a random location within the defined radius
FVector PatrolPoint = GetRandomLocation(Origin, SearchRadius);
// Set this point in the Blackboard
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
BlackboardComp->SetValueAsVector(PatrolLocationKey.SelectedKeyName, PatrolPoint);
}
return EBTNodeResult::Succeeded;
}
FVector UBTTask_FindRandomPatrolPoint::GetRandomLocation(const FVector Origin, float Radius)
{
UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
FNavLocation RandomLocation;
if (NavSys && NavSys->GetRandomPointInNavigableRadius(Origin, Radius, RandomLocation))
{
return RandomLocation.Location;
}
// If navigation point not found, return the origin
return Origin;
}
Now that we have written the codes, let’s come to how we will use this task in the behavior tree. Let’s add a selector node under the root node. Let’s add a decorator node to this node because we want to control the conditions under which the logics with this node will work. In this example, we added the blackboard check as a decorator. If the AIState is equal to AI_Patrol, the selector node will work. We added the sequence node under the selector node. Then, we added the “FindRandomPatrolPoint” task. We used the “MoveTo” task to go to the target point we found. We added the “Wait” task to stop at the point we went. Thus, we have created an AI behavior that finds random patrol points and goes to those points.
5. Service Node
The BTService_EnemyInRange checks for enemies located within a defined distance from the AI character. When an enemy is detected within this range, it updates a Blackboard key with a boolean value, signaling that the AI should engage or transition into a combat state. This service is valuable in situations where AI characters must respond to nearby threats.
- Right-click in the Content Browser > Add New > C++ Class > BTService and name it BTService_EnemyInRange.
- This class will define the behavior for check if enemy in range.
- BTService_EnemyInRange.h and define necessary components, such as references to Blackboard keys for storing the Enemy and DetectRadius of the area where the point will be found.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_EnemyInRange.generated.h"
UCLASS()
class YOURGAME_API UBTService_EnemyInRange : public UBTService
{
GENERATED_BODY()
public:
UBTService_EnemyInRange();
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
private:
// Blackboard key to store whether an enemy is in range
UPROPERTY(EditAnywhere, Category="Blackboard")
FBlackboardKeySelector EnemyInRangeKey;
// Radius within which the AI detects an enemy
UPROPERTY(EditAnywhere, Category="Settings")
float DetectionRadius = 1000.0f;
};
UBTService_EnemyInRange class, which inherits from UBTService, the base class for services in Unreal Engine’s Behavior Tree system.
TickNode is an override function that Unreal Engine calls at each interval (tick) defined for the service in the Behavior Tree editor.
EnemyInRangeKey is a BlackboardKeySelector used to reference a Blackboard key. It will be updated to true if an enemy is within the detection range.
DetectionRadius defines the maximum distance at which the AI can detect an enemy.
- BTService_EnemyInRange.cpp file implements the logic for the UBTService_EnemyInRange service, which checks if any enemies are within a specified range of an AI character and updates a Blackboard key accordingly. This service typically runs at a set interval, allowing the AI to react dynamically as enemies move in and out of range.
#include "BTService_EnemyInRange.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "GameFramework/Pawn.h"
#include "Kismet/GameplayStatics.h"
UBTService_EnemyInRange::UBTService_EnemyInRange()
{
NodeName = "Check If Enemy Is In Range";
}
void UBTService_EnemyInRange::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
AAIController* AIController = OwnerComp.GetAIOwner();
if (!AIController) { return; }
APawn* ControlledPawn = AIController->GetPawn();
if (!ControlledPawn) { return; }
bool bEnemyInRange = false;
// Find all potential enemy actors in the level (assuming they are tagged as "Enemy")
TArray<AActor*> AllEnemies;
UGameplayStatics::GetAllActorsWithTag(GetWorld(), "Enemy", AllEnemies);
// Check each enemy's distance to see if they are within DetectionRadius
for (AActor* Enemy : AllEnemies)
{
float DistanceToEnemy = FVector::Dist(ControlledPawn->GetActorLocation(), Enemy->GetActorLocation());
if (DistanceToEnemy <= DetectionRadius)
{
bEnemyInRange = true;
break; // Stop searching once an enemy is found within range
}
}
// Get the Blackboard component and update the EnemyInRangeKey
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp)
{
BlackboardComp->SetValueAsBool(EnemyInRangeKey.SelectedKeyName, bEnemyInRange);
}
}
After coding our BTService, now it is time to use it in the behavior tree. We put a selector node under the root. Then we put the “EnemyInRange” service inside this selector node. This service calculates every tick time and sets the “EnemyIsRange” blackboardKey of bool type. There are 2 more selector nodes under this selector node and these nodes have decorators. Whichever decorator value is true, that part of the tree will work. In addition, since the first selector node will always work after the Root node, our “EnemyInRange” service will also work. Thus, our AI will constantly check if there is an Enemy around it. In our example, the enemies around us are found with the UGameplayStatics::GetAllActorsWithTag() function. However, it would be better to do this according to the data coming from Perception.
We have come to the end of our article. In this article, we talked about what Behavior Tree is. We talked about how to use structures such as Root, Sequence, Selector, BTDecorator, BTTask and BTService by coding in C++ in the unreal engine. Thank you for reading this far. You can write any questions you have in the comments section below. You can Click Here to read my other articles.