W09 - Blackboard To The Future V1.01

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 14

FIT2096 - Games Programming

WEEK 9 Laboratory
BLACKBOARD TO THE FUTURE
Welcome to the Week 9 lab! This will be our second of two weeks of AI where we are
going to smart-ify our enemies even more!

Here we’re going to be creating a new AI Controller which makes use of a behaviour tree
and blackboard to manage how it reacts to the player. We’re going to add in the
functionality we had last week, but also add the ability for our enemies to shoot at our
players!

This Lab is Assessed


Labs 6 - 9 make up a component of Assignment 2a. You must complete this lab
and submit it by the due date in Week 10. More information about the
assessment can be found under the Assessments section on Moodle.

Learning outcomes this week:

● Utilising Behaviour Trees to manage AI Behaviour

● Utilising Blackboards to manage variables

● Creating custom Behaviour Tree Tasks


FIT2096 - Games Programming
Week 9: Blackboard To The Future

1. Cloning our Git Repository


The first thing we need to do is clone the local repository of our code from last week’s lab. If you are
working on your own device and already have the repository, make sure you fetch the latest changes!

To clone our repository, first open up GitHub Desktop and skip the login screen, and enter your
name and Monash email address in the next screen.

Then, click the Clone a repository from the Internet button and then switch to the URL tab. Head
to the BeatEmUp project we made on GitLab last week, and click the Clone dropdown button and
copy the Clone with HTTPS link to the clipboard. Paste this into GitHub Desktop in the
Repository URL textbox, then set the Local Path to wherever you would like to clone the repository
to. Hit the Clone button.

If you are on the lab machines: We have to clone the repository to the actual machine we are
working on, instead of the Monash network drives. There a couple of places we can choose from, but
we suggest creating a new folder named Git inside of your Downloads folder. We create a new
folder because the folder we clone to has to be an empty one.

This will open up an Authentication failed window, enter your authcate as the Username and your
Personal Access Token we created earlier as your Password. Then hit Save and retry. If you have
forgotten your Personal Access Token, you can create another one with the instructions on Page 8
of the Week 0 Supplementary.

And that’s it! Let’s get into the code!

2
FIT2096 - Games Programming
Week 9: Blackboard To The Future

2. Creating a New Enemy Controller


The first thing we’re going to start with this week is creating a new enemy controller, this time to be
used with a Behaviour Tree and Blackboard!

Go into the C++ Classes folder in your Content Drawer and then make a New C++ Class with AI
Controller as the Parent Class. Name it EnemyBTController and hit create.

Once Rider opens, the first thing we’re going to do is add one more thing to our modules like we did
last week.

In the Solution Explorer to the left of the screen, open BeatEmUp.Build.cs and add the following
module inside of the AddRange statement:
, "GameplayTasks"
Next, add the following includes up the top of EnemyBTController.h:
#include "NavigationSystem.h"
#include "Perception/AISenseConfig_Sight.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Perception/AIPerceptionComponent.h"

Then, add the following to a new public bsection:


AEnemyBTController();

virtual void BeginPlay() override;


virtual void Tick(float DeltaSeconds) override;
virtual FRotator GetControlRotation() const override;
void GenerateNewRandomLocation();

UFUNCTION()
void OnSensesUpdated(AActor* UpdatedActor, FAIStimulus Stimulus);

Most of this is very similar to what we did last week, with a couple of differences, we have removed a
couple of the old functions like OnMoveComplete, and we have added in a function to generate a
random location for us as well as made a UFUNCTION which will be called when our sight updates.

3
FIT2096 - Games Programming
Week 9: Blackboard To The Future

2. Creating a New Enemy Controller (Contd.)


After that, add the following below your new functions:
UPROPERTY(EditAnywhere)
float SightRadius = 500;
UPROPERTY(EditAnywhere)
float SightAge = 3.5;
UPROPERTY(EditAnywhere)
float LoseSightRadius = SightRadius + 50;
UPROPERTY(EditAnywhere)
float FieldOfView = 45;
UPROPERTY(EditAnywhere)
float PatrolDistance = 2000;
UPROPERTY(EditAnywhere)
UAISenseConfig_Sight* SightConfiguration;
UPROPERTY(EditAnywhere)
UBlackboardData* AIBlackboard;
UPROPERTY(EditAnywhere)
UBehaviorTree* BehaviourTree;
UPROPERTY()
UBlackboardComponent* BlackboardComponent;
UPROPERTY()
UNavigationSystemV1* NavigationSystem;
UPROPERTY()
APawn* TargetPlayer;
There are a lot of different variables here, the first 4 are setting for our enemy’s eyes, such as how
far they can see, how long they remember what they saw, and their field of view. We then have a
variable for how far we want them to patrol. After that, we have a variable for our enemy’s eyeballs, as
well as their new blackboard data, behaviour tree, and blackboard component. Lastly, we have a
reference to our navigation mesh again as well as our target player.

Make sure you Right Click and Generate Definitions for all of our new functions!

Before we head into .cpp for this class, instead head into Enemy.cpp and add the following to the
top of the Ragdoll function:
Cast<AEnemyBTController>(GetController())->BrainComponent->PauseLogic("Ragdolling!");

Also add the following to the bottom of StopRagdolling:


Cast<AEnemyBTController>(GetController())->BrainComponent->ResumeLogic("Moving Again!");
This makes it so when our enemies are ragdolling, they’re not trying to query the behaviour tree and
4
blackboard, so we pause it until we un-ragdoll. Don’t forget to add includes for
AEnemyBTController and BrainComponent!
FIT2096 - Games Programming
Week 9: Blackboard To The Future

2. Creating a New Enemy Controller (Contd.)


Heading back into EnemyBTController.cpp, add the following into the Constructor:
PrimaryActorTick.bCanEverTick = true;
SightConfiguration = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight Configuration"));
SetPerceptionComponent(*CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("Perception Component")));

SightConfiguration->SightRadius = SightRadius;
SightConfiguration->LoseSightRadius = LoseSightRadius;
SightConfiguration->PeripheralVisionAngleDegrees = FieldOfView;
SightConfiguration->SetMaxAge(SightAge);

SightConfiguration->DetectionByAffiliation.bDetectEnemies = true;
SightConfiguration->DetectionByAffiliation.bDetectFriendlies = true;
SightConfiguration->DetectionByAffiliation.bDetectNeutrals = true;

GetPerceptionComponent()->SetDominantSense(*SightConfiguration->GetSenseImplementation());
GetPerceptionComponent()->ConfigureSense(*SightConfiguration);

First here we set this actor to be able to tick as we want to be able to use its tick function, then
create its sight configuration and set its perception component to be a new perception component.
After that, we set each of our sight configuration variables, such as it’s radius, lose sight radius, field
of few, as well as what type of pawns it can see, in this case, all types of pawns. We lastly then set
our dominant sense to be our sight, and configure our sense from our sight configuration. Don’t
worry if you don’t fully understand these, as it will be almost exactly the same for your agents!

Next, add the following to BeginPlay():


NavigationSystem = Cast<UNavigationSystemV1>(GetWorld()->GetNavigationSystem());

UseBlackboard(AIBlackboard, BlackboardComponent);
RunBehaviorTree(BehaviourTree);

GetPerceptionComponent()->OnTargetPerceptionUpdated.AddDynamic(this, &AEnemyBTController::OnSensesUpdated);
Here we first get our navigation system from our world and set our own variable, then tell our AI to
use our blackboard and start running the behaviour tree, and lastly we bind our perception update to
our senses update function we made before so we can check what we actually saw!

5
FIT2096 - Games Programming
Week 9: Blackboard To The Future

2. Creating a New Enemy Controller (Contd.)


Next up, add the following to Tick():
if(TargetPlayer)
{
BlackboardComponent->SetValueAsVector("PlayerPosition", TargetPlayer->GetActorLocation());
}
Here we first check to see if we have a target player, and if so, we set our blackboard’s
“PlayerPosition” value to be our Target Player’s location so we can use it in our behaviour tree.

After that, add the following to GetControlLocation() above the super call:
if(GetPawn())
{
return FRotator(0, GetPawn()->GetActorRotation().Yaw,0);
}
This is the same as what we did last week, here we just make sure we’re following our enemy’s yaw.

Next, a new function! Add the following to GenerateNewRandomLocation():


if(NavigationSystem)
{
FNavLocation ReturnLocation;
NavigationSystem->GetRandomReachablePointInRadius(GetPawn()->GetActorLocation(), PatrolDistance, ReturnLocation);
BlackboardComponent->SetValueAsVector("PatrolPoint", ReturnLocation.Location);
}
Here we check to see if our navigation system has been set, and if so, we get a random point within
patrol distance of our enemy, then set our blackboard variable of PatrolPoint to be this location,
again so we can use it in our behaviour tree!

6
FIT2096 - Games Programming
Week 9: Blackboard To The Future

2. Creating a New Enemy Controller (Contd.)


Lastly for this class, add the following to OnSensesUpdated():
APawn* SensedPawn = Cast<APawn>(UpdatedActor);
if(SensedPawn)
{
if(SensedPawn->IsPlayerControlled())
{
if(Stimulus.WasSuccessfullySensed())
{
TargetPlayer = SensedPawn;
BlackboardComponent->SetValueAsBool("ChasePlayer", true);
BlackboardComponent->SetValueAsVector("PlayerPosition", TargetPlayer->GetActorLocation());
}
else
{
TargetPlayer = nullptr;
BlackboardComponent->SetValueAsBool("ChasePlayer", false);
}
}
}
Firstly, we check to see if the thing we saw was a Pawn, and if it was, we check to see if it is player
controller (i.e: us), if so and our stimulus was successfully sense, then we say that our target player is
this new sensed pawn, and then update our ChasePlayer and PlayerPosition in our blackboard so we
can use them in our behaviour tree. If it wasn’t successfully sensed, then we’ve lost track of our
player and we set ChasePlayer to false.

That’s it for this class, but we’re not done just yet with the coding! First head back to Unreal and hit
Compile.
3. Creating a Custom Behaviour Tree Task
Right Click in the C++ Classes again and Create a New C++ Class, this time based on
BTTaskNode as the Parent Class and name this GenerateNewRandomLocationTask.

Once that opens in Rider, add the following to GenerateNewRandomLocationTask.h in a new


public section:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
This function will be called when it gets called in our behaviour tree and it let’s us create custom
tasks! Make sure to Right Click and Generate a Definition.

7
FIT2096 - Games Programming
Week 9: Blackboard To The Future

3. Creating a Custom Behaviour Tree Task (Contd.)


Heading into GenerateNewRandomLocationTask.cpp, delete the existing super call and replace
it with:
UBehaviorTreeComponent* BTComp = &OwnerComp;
if(!BTComp)
{
return EBTNodeResult::Failed;
}

AEnemyBTController* BTController = Cast<AEnemyBTController>(BTComp->GetOwner());


if(!BTController)
{
return EBTNodeResult::Failed;
}

BTController->GenerateNewRandomLocation();

return EBTNodeResult::Succeeded;

Here we first get a reference to the behaviour tree component, and if that returns null, we say our
node has failed. If it’s not null, then we get our Enemy Behaviour Tree Controller, and again if that is
null, we say our task failed. If it doesn’t return null, then we call our GenerateNewRandomLocation
function from the controller and say our task succeeded.

Make sure to include EnemyBTController!

That’s it! Head back into Unreal and hit Compile again.

4. Setting up our Behaviour Tree


Once it finishes, Right Click on EnemyBTController and Create a Blueprint Class based on this
class and name it BP_EnemyBTController and place it in the Blueprints folder.

Open up BP_Enemy and change the AI Controller Class to BP_EnemyBTController.

After that, open the Content Drawer and then open the Blueprints folder. Right Click and then go
to Artificial Intelligence and create a Behavior Tree named EnemyBT and a Blackboard named
EnemyBlackboard.

Once these are created, open up BP_EnemyBTController and set AIBlackboard to


EnemyBlackboard and Behaviour Tree to EnemyBT.
8
FIT2096 - Games Programming
Week 9: Blackboard To The Future

4. Setting up our Behaviour Tree (Contd.)


The last couple of steps we have to do is actually set up our blackboard and behaviour tree, first
we’ll set up our blackboard!

Open up EnemyBlackboard and then in the top left, click New Key and then create a Bool and
name it ChasePlayer. Create another 2 keys for PlayerPosition and PatrolPoint but this time of
type Vector.

That’s the blackboard done! Next, head into EnemyBT.

Our behaviour tree does exactly what it says on the tin, it is a tree that reads top left to bottom right
that controls behaviour!

First, drag off from the Root and select a Selector. Selectors will call each of the nodes under it
from left to right, and will keep trying the next node even if the node before it fails.

After that, drag off the Selector to the left and this time choose a Sequence. A sequence node will
try each of its child nodes one by one, but if any of them return fail, the entire sequence will fail.

Next Right Click on the Sequence and click on Add Decorator->Blackboard. Decorators are ways
we can add conditions to our behaviour tree, and in this case, we only want our enemies to chase
our player if they can see them, so select the Decorator and then change Blackboard Key to be
ChasePlayer, and then change the Observer Aborts to Lower Priority. This will make it so if our
ChasePlayer is ever set, it will abort anything lower priority than this, so we’ll stop patrolling and
actually chase our player.

Next, drag off to the left from our sequence and select a Rotate to Face BB Entry, then change
Blackboard Key to be PlayerPosition. This will make our enemy rotate towards our player. Then
drag off the sequence again, this time to the right of the existing node, and choose a Move To
node. Change the Blackboard Key to PlayerPosition and tick Observe Blackboard Value. This
will make it so our enemy will chase our player, and ticking observe blackboard value makes it so if
the value updates, the move to updates as well.

After this, back up to the top Selector node, drag off to the right of the existing nodes, and create
another sequence, then drag off to the left and select Generate New Random Location Task
which will call our custom task. Then drag another three nodes off from the sequence and select in
order: Rotate to Face BB Entry, Move To, and Wait. Set the Blackboard Key for both the Rotate
to Face BB Entry and the Move To Node to be PatrolPoint and then set the Wait time to be 1s.

That’s it, your first behaviour tree should look like the image to the right and you should be able to
test it out, and get pretty much the same behaviour as last week!
9
FIT2096 - Games Programming
Week 9: Blackboard To The Future

5. Giving our Enemies a Ranged Attack


The main benefit of behaviour trees over Finite State Machines like we used last week is that they’re
easily expandable. If we wanted to add new behaviour to our FSM, we’d need to change all of the
transitions between states, whereas in a behaviour tree, we can just drag the nodes around a bit… so
let’s give our enemies a ranged attack!

Open up the Content Drawer, go to the C++ Classes folder and Right Click to Create a New C++
Class that inherits from Actor as the parent, named Bullet.

Once that opens, add the following to a public section in Bullet.h:


UPROPERTY(EditAnywhere)
UProjectileMovementComponent* MovementComponent;
UPROPERTY(EditAnywhere)
UStaticMeshComponent* Mesh;
UPROPERTY(EditAnywhere)
float MovementSpeed = 2000;
UPROPERTY(EditAnywhere)
float Damage = 5;
UFUNCTION()
void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector Normal, const FHitResult& Hit);
Here we first have a reference to a handy movement component that will handle our projectile, then
a reference to our mesh as well as a couple of variables for our movement speed and damage. Lastly
we have an OnHit function to deal some damage to our player, or enemies!

Make sure to Right Click and Generate a Definition for this function and also add an include for
ProjectileMovementComponent!

Heading into Bullet.cpp, add the following to the Constructor:


Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
RootComponent = Mesh;
MovementComponent = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("Movement Component"));

MovementComponent->bShouldBounce = true;
MovementComponent->BounceVelocityStopSimulatingThreshold = MovementSpeed / 2;
MovementComponent->bSweepCollision = true;
MovementComponent->InitialSpeed = MovementSpeed;
MovementComponent->UpdatedComponent = Mesh;
MovementComponent->ProjectileGravityScale = 0;

Here like normal we create our components, that being our mesh as well as our movement
10
component. We then set our movement component’s variables, such as how it bounces, how it
collides, its initial speed as well as what component it should move.
FIT2096 - Games Programming
Week 9: Blackboard To The Future

5. Giving our Enemies a Ranged Attack (Contd.)


Next, inside of BeginPlay, add:
Mesh->OnComponentHit.AddDynamic(this, &ABullet::OnHit);
Mesh->SetNotifyRigidBodyCollision(true);

This will add our OnHit function to the OnComponentHit event, and also make sure it actually calls
hit events on our mesh.

Lastly, add the following to OnHit:


if(OtherActor && OtherActor != this)
{
AEnemy* HitEnemy = Cast<AEnemy>(OtherActor);
if(HitEnemy)
{
HitEnemy->DealDamage(Damage);
Destroy();
}
ABeatEmUpCharacter* HitCharacter = Cast<ABeatEmUpCharacter>(OtherActor);
if(HitCharacter)
{
HitCharacter->DealDamage(Damage);
Destroy();
}
}
Here we check to see if we hit an enemy or player, and if so, deal some damage and destroy the
bullet! Nice and simple!

Make sure to add an include for Enemy inside the .cpp!

Next, open Enemy.h and add the following inside a public section:
UPROPERTY(EditAnywhere)
TSubclassOf<ABullet> BulletClass;
void Shoot(FVector Direction);

Here we add a variable to store our bullet class (don’t forget the include!) and then a function to
actually shoot the bullet.

Make sure to Create a Definition!

11
FIT2096 - Games Programming
Week 9: Blackboard To The Future

5. Giving our Enemies a Ranged Attack (Contd.)


Next, add the following inside of Shoot:
FVector SpawnLocation = GetActorLocation() + GetActorForwardVector() * 100;
FRotator SpawnRotation = Direction.Rotation();
ABullet* SpawnedBullet = Cast<ABullet>(GetWorld()->SpawnActor(BulletClass, &SpawnLocation, &SpawnRotation));
Direction.Normalize();
SpawnedBullet->MovementComponent->Velocity = Direction * SpawnedBullet->MovementSpeed;
We first spawn our bullet slightly in front of our enemy, then set our bullet’s velocity to be in the
direction we want to shoot of size movement speed.

Almost done here, next, head into EnemyBTController.h and add the following to a public section:
UPROPERTY(EditAnywhere)
float Ammo = 5;
void Shoot();

Just a quick variable to store the amount of ammo our enemies have, then a function to actually
shoot which we can call from a new custom task. Make sure to Generate a Definition!

Heading into EnemyBTController.h, add the following to Shoot:


FVector ShootDirection = TargetPlayer->GetActorLocation() - GetPawn()->GetActorLocation();
Cast<AEnemy>(GetPawn())->Shoot(ShootDirection);
Ammo--;
BlackboardComponent->SetValueAsBool("HasAmmo", Ammo > 0);
Here we first get a vector in the direction of our player, then call the shoot function from our enemy
(don’t forget the include!). Lastly we update our ammo and set our blackboard value HasAmmo to
be true if we have any, and false if we don’t.

Lastly here, add the following to BeginPlay:


BlackboardComponent->SetValueAsBool("HasAmmo", true);
That’s it for these classes, though we do need a task to actually shoot!

Head back into Unreal and Compile, then Create a New C++ Class based on BTTaskNode again,
this time named ShootTask.

Once Rider opens, add the following to ShootTask.h in a public section:


virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
Make sure to Generate a Definition here!
12
FIT2096 - Games Programming
Week 9: Blackboard To The Future

5. Giving our Enemies a Ranged Attack (Contd.)


For the last of our code, add the following inside of ExecuteTask inside of ShootTask.cpp (replacing the Super return call):
UBehaviorTreeComponent* BTComp = &OwnerComp;
if(!BTComp)
{
return EBTNodeResult::Failed;
}

AEnemyBTController* BTController = Cast<AEnemyBTController>(BTComp->GetOwner());


if(!BTController)
{
return EBTNodeResult::Failed;
}

BTController->Shoot();

return EBTNodeResult::Succeeded;

Here we’re doing almost exactly the same thing as in our previous task, but this time we’re calling Shoot instead… that’s it! Make sure to add an include for AEnemyBTController!

Head back to Unreal and hit Compile again.

Once that finishes, Right Click on our Bullet class and Create a Blueprint based on it named BP_Bullet saved in the Blueprints folder.

Open up BP_Bullet and set the Mesh Static Mesh to be Shape_Sphere and then set the Scale to be 0.1 for all axes.

Next, open BP_Enemy and set the Bullet Class to be BP_Bullet.

Open up EnemyBlackboard and add a New Bool Key named HasAmmo.

After that, open up EnemyBT and disconnect the Rotate and MoveTo nodes from the ChasePlayer sequence by holding Alt and clicking on the arrows. Move them out of the way
for now.

Drag off a Selector node from our ChasePlayer sequence.

Next, drag off 2 Sequence Nodes from the new Selector node. On the leftmost one, drag off a Shoot Task and a Simple Parallel. Right click on the Shoot Task and add 2
Decorators: Blackboard and Cooldown. Set the Blackboard Key to be HasAmmo and set the Cool Down Time to be 1s. This will make it so we will only shoot if we have ammo,
and only once every second.

Then, from the purple (left) side of the Simple Parallel, drag off a Wait node and set that to 1s, then from the grey (right) side, drag off a Rotate to Face BB Entry and set the
Blackboard Key to be PlayerPosition.

Lastly, connect the Rotate and Move To Player Position nodes we disconnected earlier to the empty right sequence. 13
FIT2096 - Games Programming
Week 9: Blackboard To The Future

5. Giving our Enemies a Ranged Attack (Contd.)


Your final behaviour tree should look like the image to the right—-------------------------->

Give it a test! Your enemies should try and shoot at you now until they run out of ammo
then rush you like before!

6. Additional Tasks
For this week the we want you to have a think about what additional Enemy
Type/Behaviour you’ll be adding for your assignment.

Make sure to pitch this to your demonstrator!

The rubric for this week is shown below:

Mark Explanation
0 Student was absent or has not attempted lab tasks

0.5 An attempt has been made at the lab tasks but they are unfinished

1 Lab tasks have been successfully completed

1.5 New Enemy Type/Behaviour Started

2 New Enemy Type/Behaviour Completed

14

You might also like