In tutorial, im going to show, step by step, how to create a very simple zombie AI that is idle until it sees a player, and when it sees it it goes to attack it melee.
The behavior tree is a very interesting feature, still WIP, and really barebones, you will find that the default tasks are really few and more are needed most of the time.
The system works by selecting a task to run, and execute it. When the task is finished, it searches for the next task to do.
Tasks
The Tasks are derived from BTTask_Blackboard if you want to create them in c++, and BTTask_Blueprint if you want to create them in blueprints. For my needs, most of the time ive found that keeping them in blueprints helps a LOT with prototyping, if i need a task that does strange things, i code the difficult part on C++, most of the times in my base AI Controller class or Bot Pawn, and then i call that function from blueprint. If you are worried of performance, you can still make the task purely C++ if you want.
Services
A service is some kind of minitask, that is called all the time. Its what i use to check sight, i have my ZombieSearchPlayer Service wich just calls the SearchPlayer() function in my c++ code( as its a bit more complex logic, i made it as c++ function). That service is called on a interval of time, you can set it one time and some random offset. Of course, if you use lower intervals, calling the service many times, you can slow the game, so better have care to what you are calculating. One example is that my SearchPlayer function was not very well optimized, as it searches all the pawns in the map and checks if one is a player, and calling that every 0.1 seconds on 15 zombies isnt good to the performance, so i added a check and only search for a player if you dont have a target already)
Composites
A Composite selects wich task to run, by default, you will see a number in the composites and tasks, thats is the order the behavior tree will check each task. The Sequence composite is one of the most useful, it makes tasks run one after another. Selector only runs a task if the one before failed. And Simple Parallel is the one to use if you want several tasks executing at once ( im using it for the melee attack, one task performs the damage and the animation, while other makes the bot run to target)
Decorators
You can attach Decorators to both Composites and Tasks. The Decorator is to control the flow, for example a very useful one is the Blackboard Decorator. In that one you can make it so it only makes the task in that branch execute if some blackboard value is set or not. The cool thing about Decorators is that you can make them as some kind of “event”. The Blackboard Decorator can override a different task that is runnning, if you set “Observer aborts” property to Both, it overrides its branch and every other branch that has lower priority (its to the right), If you click the Decorator, you will see that some nodes get colored, the ones that can be overriden. In the zombie logic, when the Enemy blackboard variable is set, the decorator is fired and cancels the default “wait” task.
Blackboard
The blackboard is VERY important, its a data asset, that you need to create in the editor. It holds the values your behavior tree uses. In the zombie example, it has Enemy variable only, as its the only needed. But in more complex AI i have, i have more than 10 values in the blackboard to drive different Decorators and add variables to tasks.
Tasks get their data from the Blackboard, yes, you could access the controller or pawn easily and use the values there. But if the data is in the blackboard, you can use it to drive Decorators, and see the variables at runtime easily when debugging.
Example
Enough theory, lets go to the actual example. The plan is a very basic zombie/creature/whatever AI that stands still until it sees a player, and if it sees the player it goes to hit him in the face.
You should do with ShooterGame example, editing it, or with a project that uses similar code. Becouse the shootergame already provides bot class that can die and take damage, wich is useful for the AI.
To start, make sure you have Behavior Tree Editor enabled. To enable it, go to Edit->Editor Preferences-> Experimental. Check the Behavior Tree Editor property.
When the BTEditor is enabled, create 2 new assets in your content browser. One of type Behavior Tree , and other of type Data Asset, use class Blackboard when it asks wich data asset class to use. Name them something logical, like SimpleZombieBT, and SimpleZombieBlackboard
Now, lets go to the Blackboard Data Asset. You will get a property window. Ignore the “Parent” tab, and in the “Keys” array, add one value, set the KeyType to type Object, and the name to Enemy, also, in the BaseClass under Key Type once you set that as object, i recomend you to set it as Character or your base player class, for safety.
Once that is set, go to the Behavior tree asset, and open it. You will be welcomed with a blueprint like grid with only one ROOT node.
Click that node, and set the Blackboard Asset value to the blackboard you just created. After that click in the dark grey bar on the lower part of the ROOT node, drag it( an arrow should appear), and like in blueprints, it prompts a menu. Select a “Sequence”, will be our basic sequence for the character.
Drag from the lower dark grey bar of the Sequence node, and create another sequence node. In that node, right click it, and select Add Decorator. Add a Blackboard Decorator. Click that decorator(blue), and see the properties.
That decorator will fire its branch whenever the “Enemy” key in the blackboard is set.
Now, add some pathfinding logic to the zombie, so it runs to the Enemy
Add a MoveTo Task node, and connect it to the Attack logic. In the task properties, put Acceptable Radius as 100, its a good value to end the pathfinding, then the hit logic will trigger.
Makse sure the Blackboard Key of the Move To node is set as Enemy. Becouse we obviously want the AI to move to the Enemy position.
Add a SimpleParallel composite attached to the sequence that has the blackboard.
That node makes a base task to run (left bottom grey bar), and while it runs it also runs the tasks attached to the right side. When the task finishes, the secondary tasks are stopped.
Add a MoveDirectlyToward task at the right side, set Acceptable Radius to 0, and make sure the blackboard key is Enemy.
Why 2 different “move” tasks?
Easy, becouse Move To uses actual pathfinding, while MoveDirectlyTowards doesnt. Thats why we are using Move To if its far, and Movedirectlytowards when its close.
At moment, the behavior tree looks like this:
If you try to run it, it will do nothing. Yet.
C++ Code
The important logic for the Melee attack and the Search Enemy functions are written in my own ACreatureAI and ACreatureBot classes. ACreatureAI inherits from AAIController and ACreatureBot from AShooterCharacter.
is the CreatureAI.h
UCLASS()
class ACreatureAI : AAIController
{
GENERATED_UCLASS_BODY()
UPROPERTY(transient)
TSubobjectPtr<class UBlackboardComponent> BlackboardComp;
UPROPERTY(transient)
TSubobjectPtr<class UBehaviorTreeComponent> BehaviorComp;
virtual void Possess(class APawn* InPawn) OVERRIDE;
virtual void BeginInactiveState() OVERRIDE;
void Respawn();
UFUNCTION(BlueprintCallable, Category = Behavior)
void SetEnemy(class APawn* InPawn);
UFUNCTION(BlueprintCallable, Category = Behavior)
class AShooterCharacter* GetEnemy() const;
UFUNCTION(BlueprintCallable, Category = Behaviour)
bool PawnCanBeSeen(APawn * target);
/* Checks sight to all pawns in map, sets enemy if it finds a thing */
UFUNCTION(BlueprintCallable, Category = Behaviour)
void SearchEnemyInView();
protected:
int32 EnemyKeyID;
};
The Cpp file is like this
#include "ShooterGame.h"
ACreatureAI::ACreatureAI(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP)
{
// create blackboard and behaviour components in the constructor
BlackboardComp = PCIP.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT("BlackBoardComp"));
BehaviorComp = PCIP.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT("BehaviorComp"));
bWantsPlayerState = true;
}
void ACreatureAI::Possess(APawn* InPawn)
{
Super::Possess(InPawn);
ACreatureBot* Bot = Cast<ACreatureBot>(InPawn);
// start behavior
if (Bot && Bot->BotBehavior)
{
BlackboardComp->InitializeBlackboard(Bot->BotBehavior->BlackboardAsset);
// Get the enemy blackboard ID, and store it to access that blackboard key later.
EnemyKeyID = BlackboardComp->GetKeyID("Enemy");
BehaviorComp->StartTree(Bot->BotBehavior);
}
}
void ACreatureAI::BeginInactiveState()
{
Super::BeginInactiveState();
AGameState* GameState = GetWorld()->GameState;
const float MinRespawnDelay = (GameState && GameState->GameModeClass) ? GetDefault<AGameMode>(GameState->GameModeClass)->MinRespawnDelay : 1.0f;
GetWorldTimerManager().SetTimer(this, &ACreatureAI::Respawn, MinRespawnDelay);
}
void ACreatureAI::Respawn()
{
// GetWorld()->GetAuthGameMode()->RestartPlayer(this);
}
void ACreatureAI::SetEnemy(class APawn* InPawn)
{
if (BlackboardComp)
{
BlackboardComp->SetValueAsObject(EnemyKeyID, InPawn);
SetFocus(InPawn);
}
}
class AShooterCharacter* ACreatureAI::GetEnemy() const
{
if (BlackboardComp)
{
return Cast<AShooterCharacter>(BlackboardComp->GetValueAsObject(EnemyKeyID));
}
return NULL;
}
void ACreatureAI::UpdateControlRotation(float DeltaTime, bool bUpdatePawn)
{
// Look toward focus
FVector FocalPoint = GetFocalPoint();
if (!FocalPoint.IsZero() && GetPawn())
{
FVector Direction = FocalPoint - GetPawn()->GetActorLocation();
FRotator NewControlRotation = Direction.Rotation();
NewControlRotation.Yaw = FRotator::ClampAxis(NewControlRotation.Yaw);
SetControlRotation(NewControlRotation);
APawn* const P = GetPawn();
if (P && bUpdatePawn)
{
P->FaceRotation(NewControlRotation, DeltaTime);
}
}
}
bool ACreatureAI::PawnCanBeSeen(APawn * target)
{
if (target == NULL || GetPawn() == NULL)
{
return false;
}
FVector difference = target->GetActorLocation() - GetPawn()->GetActorLocation();
float angle = FVector::DotProduct(difference, GetPawn()->GetActorRotation().Vector());
if (LineOfSightTo(target, GetPawn()->GetActorLocation()) && angle >0)
{
return true;
}
else
{
return false;
}
}
void ACreatureAI::SearchEnemyInView()
{
APawn* MyBot = GetPawn();
if (MyBot == NULL)
{
return;
}
const FVector MyLoc = MyBot->GetActorLocation();
float BestDistSq = MAX_FLT;
AShooterCharacter* BestPawn = NULL;
//foreach all pawns in world
for (FConstPawnIterator It = GetWorld()->GetPawnIterator(); It; ++It)
{
UE_LOG(LogShooterWeapon, Log, TEXT(" ENEMY SEEN %s "), *GetNameSafe(*It));
if (PawnCanBeSeen(*It))
{
AShooterCharacter* TestPawn = Cast<AShooterCharacter>(*It);
if (TestPawn && TestPawn->IsAlive() && Cast<ACreatureBot>(TestPawn) == NULL)
{
const float DistSq = (TestPawn->GetActorLocation() - MyLoc).SizeSquared();
if (DistSq < BestDistSq)
{
BestDistSq = DistSq;
BestPawn = TestPawn;
}
}
}
}
if (BestPawn)
{
// We saw someone, so set him as target.
SetEnemy(BestPawn);
}
}
The important thing is the Search Enemy in View function, wich checks EVERY pawn in the map, and checks if it can be seen (PawnCanBeSeen function). If the pawn is visible, it casts it to CreatureBot, wich is the AI class, and if it isnt a AI class, then it can be a target, so store it. Get the closest pawn, and target that one. The SetEnemy function writes the enemy pawn to the blackboard, so it fires the decorators in the trees and can be used for the MoveTo and MoveToward classes.
The CreatureBot class is actually much simpler, as its just a character inheriting from ShooterCharacter, wich has the PerformMelee attack function and its several variables.
CreatureBot.h
UCLASS()
class ACreatureBot : AShooterCharacter
{
GENERATED_UCLASS_BODY()
UPROPERTY(EditDefaultsOnly, Category = Behaviour)
float AttackRange;
UPROPERTY(EditDefaultsOnly, Category = Behaviour)
float AttackDamage;
UPROPERTY(EditAnywhere, Category = Behavior)
class UBehaviorTree* BotBehavior;
UFUNCTION(BlueprintCallable, Category = Behavior)
void PerformMeleeAttack();
float AccumulatedFiretime;
virtual bool IsFirstPerson() const OVERRIDE;
virtual void FaceRotation(FRotator NewRotation, float DeltaTime = 0.f) OVERRIDE;
bool Attacking;
};
CreatureBot.cpp
// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "ShooterGame.h"
ACreatureBot::ACreatureBot(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP)
{
AIControllerClass = ACreatureAI::StaticClass();
UpdatePawnMeshes();
AttackRange = 100;
AttackDamage = 10;
bUseControllerRotationYaw = true;
}
bool ACreatureBot::IsFirstPerson() const
{
return false;
}
void ACreatureBot::FaceRotation(FRotator NewRotation, float DeltaTime)
{
FRotator CurrentRotation = FMath::RInterpTo(GetActorRotation(), NewRotation, DeltaTime, 8.0f);
Super::FaceRotation(CurrentRotation, DeltaTime);
}
void ACreatureBot::PerformMeleeAttack()
{
const FVector StartTrace = GetActorLocation();
const FVector ShootDir = GetActorRotation().Vector();
const FVector EndTrace = StartTrace + ShootDir * AttackRange;
// We perform a sphere sweep, checking if there is something in the cylinder that trace creates, and if it finds something, damage it.
static FName WeaponFireTag = FName(TEXT("WeaponTrace"));
FCollisionQueryParams TraceParams(WeaponFireTag, true);
TraceParams.AddIgnoredActor(this);
TraceParams.bTraceAsyncScene = true;
TraceParams.bReturnPhysicalMaterial = true;
FHitResult Hit(ForceInit);
GetWorld()->SweepSingle(Hit, StartTrace, EndTrace, FQuat::Identity, ECollisionChannel::ECC_Pawn, FCollisionShape::MakeSphere(25), TraceParams);
if (Hit.Actor != NULL)
{
ACharacter *character = Cast<ACharacter>(Hit.GetActor());
if (character != NULL)
{
FPointDamageEvent PointDmg;
PointDmg.DamageTypeClass = UDamageType::StaticClass();
PointDmg.HitInfo = Hit;
PointDmg.ShotDirection = ShootDir;
PointDmg.Damage = AttackDamage;
character->TakeDamage(AttackDamage, PointDmg, Controller, this);
}
}
}
The C++ part is now completed, time to actually make the tasks in blueprint, and add them to the behavior tree
Blueprint tasks
Now its time to add the actual “damage melee” task, and the “search player” service.
Create 2 new blueprints, one inheriting from BTTask_blueprintBase, and other from BTService_BlueprintBase.
Call the Task “Melee Attack”, and the Service “Search Enemy”.
Open the Search Enemy blueprint, and make it like this.
The Receive Tick function is not like a normal actor tick that its ticked every frame, tick is ticked by the interval you put in the service properties.
Now, Open the MeleeAttack blueprint, one is QUITE a bit more complicated, becouse i made it work with animation, and not attack instantly, instead, it starts the animation and it performs the damage check a bit later, driven by the AttackTime variable.
First part, the Receive Execute event.
event is called when the task is started, only once per task execution. Here, we use it to play the attack animation
The “Attack Delta” variable is a private float variable to control the time, as they “delay” node doesnt really work properly here, so we add the time logic in tick. The AttackAnimation variable is a editable animation montage.
The second part is the Tick event.
In "timer logic"we increase AttackDelta by DeltaSeconds, and when its more thatn Attack Time (editable float variable) we fire the actual attack logic.
The atack logic just casts the controller to get the pawn, and the pawn to get the PerformAttack function, to call it. then just end the task, calling “Finish Execute” with Success set to true.
With this, our 2 blueprints are done. yes, they can be 100 c++ perfectly fine, but with this, if i think i want to make the attacker spawn a particle effect when it attacks, or attach a light when the zombie searches, i can prototype it very fast.
Completing the Behavior Tree
Now that we have our 2 blueprints to search player and hit him in the face, we can complete the behavior tree at last. Open it.
Add the MeleeAttack task to the SimpleParallel node, under the left bar, wich should be of a different color
also right click on the topmost “Sequence” node, the one that doesnt have the decorator, and add a service, “Search Enemy”. then set a normal rate for it to Tick, 0.2 seconds is fine
The final look of our behavior tree is like this
Thats it, its done. To use it, create a new blueprint for CreatureBot, and set the Blackboard property to SimpleZombieBT. The engine should do the rest for you. Cool factor is even better if you have the behavior tree opened when the game is running, as you will see wich tasks are being executed and more data.
Yes, is insanely overkill by a simple AI such as zombie/creature . But system shines when you create a complex thing, as it simplifies quite a bit it, giving editor support for everything without having to add a shitload of properties to the controller, and lots of c++ code to deal with the behavior yourself.
As allways, if you have any doubt, problem, or similar, feel free to comment it here, and ill answer it.