While our game may be running without any issues in the editor or even in…
Creating EQS Contexts in C++
In this post we’re going to explore the basics of the Environment Query System (EQS) that resides in UE4. To fully understand this post you need prior knowledge of Behavior Trees and Blackboards since these concepts won’t be explained here. In case you don’t quite remember how these work, check out Epic’s official documentation as well as my previous post on how to create a basic patrol AI.
In this post, we’re going to create a clone of Epic’s Advanced AI stream tutorial but instead of using Blueprints, we’re going to code our logic in c++. Basically, we’re going to create an AI that is scared of our player and flees when it sees him.
Before we go any further, take a look at the final result in the video below.
Moreover, please note that this tutorial was written in 4.13.1 version so you may need to adjust your code based on the version of the engine you’re using.
I’m going to explain in detail what EQS actually is, the moment we use it in our Behavior Tree. But first, let’s write some code.
Creating the AI Controller
Create a Third Person C++ Template Project and create a C++ class that inherits the AIController, named MyAIController. Before you continue, create a NavMesh that covers the whole map.
Open up the header file of your class and make sure to add the following includes before the .generated.h file:
#include “Perception/AIPerceptionComponent.h”
#include “Perception/AISenseConfig_Sight.h”
Then, type in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
private: /** BlackboardComponent - used to initialize blackboard values and set/get values from a blackboard asset */ UBlackboardComponent* BlackboardComp; /** BehaviorTreeComponent - used to start a behavior tree */ UBehaviorTreeComponent* BehaviorTreeComp; /** Blackboard Key Value Name */ const FName BlackboardEnemyKey = FName("Enemy"); /** The function that fires when the perception of our AI gets updated */ UFUNCTION() void OnPerceptionUpdated(TArray<AActor*> UpdatedActors); /** A Sight Sense config for our AI */ UAISenseConfig_Sight* Sight; protected: /** The Behavior Tree that contains the logic of our AI */ UPROPERTY(EditAnywhere) UBehaviorTree* BehaviorTree; /** The Perception Component of our AI */ UPROPERTY(VisibleAnywhere) UAIPerceptionComponent* AIPerceptionComponent; public: AMyAIController(); virtual void Possess(APawn* InPawn) override; /** Returns the seeing pawn. Returns null, if our AI has no target */ AActor* GetSeeingPawn(); |
Then, open up the source file of your class and add the following includes:
#include “BehaviorTree/BlackboardComponent.h”
#include “BehaviorTree/BehaviorTreeComponent.h”
#include “BehaviorTree/BehaviorTree.h”
#include “EqsTutCharacter.h”
Depending on how you named your project, you may have to edit the last include.
When you’re done with the included files, type in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
void AMyAIController::OnPerceptionUpdated(TArray<AActor*> UpdatedActors) { //If our character exists inside the UpdatedActors array, register him //to our blackboard component for (AActor* Actor : UpdatedActors) { if (Actor->IsA<AEqsTutCharacter>() && !GetSeeingPawn()) { BlackboardComp->SetValueAsObject(BlackboardEnemyKey, Actor); return; } } //The character doesn't exist in our updated actors - so make sure //to delete any previous reference of him from the blackboard BlackboardComp->SetValueAsObject(BlackboardEnemyKey, nullptr); } AMyAIController::AMyAIController() { //Components Init. BehaviorTreeComp = CreateDefaultSubobject<UBehaviorTreeComponent>(FName("BehaviorComp")); BlackboardComp = CreateDefaultSubobject<UBlackboardComponent>(FName("BlackboardComp")); AIPerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(FName("PerceptionComp")); //Create a Sight Sense Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config")); Sight->SightRadius = 1000.f; Sight->LoseSightRadius = 1500.f; Sight->PeripheralVisionAngleDegrees = 130.f; //Tell the sight sense to detect everything Sight->DetectionByAffiliation.bDetectEnemies = true; Sight->DetectionByAffiliation.bDetectFriendlies = true; Sight->DetectionByAffiliation.bDetectNeutrals = true; //Register the sight sense to our Perception Component AIPerceptionComponent->ConfigureSense(*Sight); } void AMyAIController::Possess(APawn* InPawn) { Super::Possess(InPawn); if (BehaviorTree) { //Initialize the Blackboard and start the attached behavior tree BlackboardComp->InitializeBlackboard(*BehaviorTree->BlackboardAsset); BehaviorTreeComp->StartTree(*BehaviorTree); } //Register the OnPerceptionUpdated function to fire whenever the AIPerception get's updated AIPerceptionComponent->OnPerceptionUpdated.AddDynamic(this, &AMyAIController::OnPerceptionUpdated); } AActor* AMyAIController::GetSeeingPawn() { //Return the seeing pawn UObject* object = BlackboardComp->GetValueAsObject(BlackboardEnemyKey); return object ? Cast<AActor>(object) : nullptr; } |
Save and compile your code.
Creating the Blackboard of our AI
Create the following Blackboard:
Make sure that your Enemy key has the Actor class as its Base Class as seen in the screenshot above.
Activating the Environment Query System
The EQS is still an experimental system and we explicitly need to enable it by changing some options inside the editor in order to use it on our Behavior Tree. To activate the EQS in 4.13 version of the engine, perform the following steps:
- Click on the Edit menu
- Open up the Editor Preferences
- Select the Experimental tab
- Check the Environment Query System
The following picture sums up the above steps:
Creating an Environment Query
If you activated EQS by following the steps above, right click somewhere on the content browser and create an Environment Query (it’s located inside the Artificial Intelligence tab only after when you enable the system) named FindHidingSpot. Save it and close it. We won’t add any logic to it just yet because we haven’t explained what a Query actually is. Please bare with me for a few moments and everything will become clear!
Creating the Behavior Tree for our AI
Create the following Behavior Tree for our AI:
The Run EQS Query node is located inside the available tasks of the behavior tree. You will notice that of the node’s properties in the details panel are marked as red. I will explain what these are in the following section.
What is the Environment Query System (EQS)?
The EQS is a system that allows our AI to “ask” the game’s environment specific questions and based on the received answers it will be able to act accordingly. An Environment Query consists of:
- Generators and
- Tests
Most Generators are used to generate an area around a specified actor in our game. This area consists of points and their density is configurable through the corresponding editor. Moreover, the developer is able to configure the size of the area as well. Each time our AI runs an Environment Query, the system iterates through all the generated points in order to find a result. We’re going to specify what this result is going to be, for example, it can be another Actor, or a world space location, etc.. This is where Tests come in play.
Think of Tests as functions that filter and/or (depending on your needs) score every generated point. Tests are attached to generators.
Based on the attached Tests, each point has a score. The point that has the higher score is considered the best match for our result.
To sum up the theory written above, here is a screenshot of how the EQS works:
If you take a second look at our Behavior Tree in the previous section you will notice that I have selected the HidingSpot as a Blackboard Key for our EQS. This means that in this case, the result of the FindHidingSpot Query is going to get stored on the HidingSpot key of our Blackboard.
Moreover, I’ve assigned the FindHidingSpot as a Query template. The Query template in this case is the query that it’s going to execute when the Run EQS Query node fires.
Last but not least, you will notice that in the RunMode in the details panel, I have selected the Single Best Item. This means that I want to store the point with the highest score only.
With that said, let’s create type some logic inside our Query.
Creating the FindHidingSpot Query
The logic that we want to create, is summed up with the following senteces:
Find a location where the enemy (in this case the player) can’t see you. This location needs to be as close as possible to the AI and at the same time, as far as possible from the enemy (in this case, the player).
To create the mentioned logic, open up the FindHidingSpot Query. Then, select the Points: Pathing Grid generator. Remeber that I mentioned that you can adjust the density of the points as well as the size of the generated area. Here are the options that enable you to do so:
You will notice that the Editor mentions that the PathingGrid will be generated around Querier. In our case, this means that a of size 2000 units is going to get generated around our AI.
Right Click on your Pathing Grid and add a Trace Test.
This is a test that performs a trace among two actors. The starting Actor is the Querier and the other end of the trace is by default our Querier again. With that said, we need to change that in order to match our needs. Specifically, we need to trace from our AI to our Player. To do that, we need to create a new Context for that test. A Context in this case is a class that contains a function, responsible for providing results in our test.
To add a context, add a new C++ class, named FindEnemyQueryContext that inherits the EnvQueryContext class. Then, inside its header file, declare the following function:
virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;You don’t need to specify an access modifier in this case. In the source file, add the following includes:
#include “EnvironmentQuery/EnvQueryTypes.h”
#include “EnvironmentQuery/Items/EnvQueryItemType_Actor.h”
#include “EqsTutCharacter.h”
#include “MyAIController.h”
Then, type in the following implementation of the ProvideContext function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void UFindEnemyQueryContext::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const { Super::ProvideContext(QueryInstance, ContextData); //Get the Owner of this Query and cast it to an actor //Then, get the actor's controller and cast to it our AIController //This code works for our case but avoid that many casts and one-liners in cpp. AMyAIController* AICon = Cast<AMyAIController>((Cast<AActor>((QueryInstance.Owner).Get())->GetInstigatorController())); if (AICon && AICon->GetSeeingPawn()) { //Set the context SeeingPawn to the provided context data UEnvQueryItemType_Actor::SetContextHelper(ContextData, AICon->GetSeeingPawn()); } } |
This will return the seeing pawn (if any) to our context. Comple and save your code.
Then, select the Trace test and in the context drop-down menu, select the FindenemyQueryContext class. Moreover, adjust the item height offset, to match the character’s height:
When the Trace test is done, it will generate points that the player can’t see from his current position. The next step, is to select the point that is close enough to our AI and far enough from our player.
To do that, add a Distance Test to our PathingGrid and select the following options:
By selecting Inverse Linear, the distance test will prefer the points that are close to our AI. Note that the Test Purpose is to Score the points only and not to filter them.
Then, add another Distance Test to our Pathing Grid. this time however, we want to get the points that are far from the player. To do that, in the Distance To dropdown menu, select our FindEnemyQueryContext and set the Scoring Equation to Linear as the following screenshot suggests:
We’re done with our Query. To test your AI:
- Create a Blueprint Controller based on MyAIController (BP_AICon)
- Create a Blueprint Character based on the Character class and attach the:
- SK_Mannequin to it’s static mesh
- ThirdPerson_AnimBP to it’s anim class
- BP_AICon to it’s AIController menu
- Assign our Behavior Tree to the BP_AICon
- Place the Blueprint character in the map
Then, click the play button and test your functionality!
Debugging your AI
To debug your AI during play, open up the project settings and inside the Gameplay Debugger tab, activate the following options:
Moreover, activate the following option from the viewport:
Then, when playing, press the ” ‘ ” (apostrophe key) on your keyboard. You don’t to write it in the console like the previous versions.
Hey man just read the tutorial, great stuff! Thanks for listening to my suggestion 😉 just one thing: one of the screenshots that shows the distance test of the Environment Query is the same as the previous one.
You’re right Alex! The post is now updated with the correct screenshot.
-Orfeas
Orfeas,
Thanks for your EQS explanation. I usually need to go around for straight explanations for Unreal terms.
-Jaime
Hey. An interesting tutorial.
However, I can´t understand where do you assign value to your behaviortree. It has UPROPERTY(EditAnywhere), but unless you create blueprint based on the AIController class you created, I find no other way to assign your created behaviortree to the one you have in the AIController class.
Could you please clarify that.
Hello,
I’ve actually created a Blueprint based on the C++ class and assigned a reference through the Editor
-Orfeas
Hi, just wanted to say this is the most useful post i’ve read on the perception and eqs from a c++ point.
Thank you for laying it out so well!
Cheers!
Hey! nice tuto. I was trying the same, but got this error:
candidate function [with UserClass = AEnemiController_AIController] not viable: no known conversion from ‘void (AEnemiController_AIController::*)(TArray)’ to ‘typename FDelegate::TMethodPtrResolver::FMethodPtr’ (aka ‘void (AEnemiController_AIController::*)(const TArray &)’) for 2nd argument
It is saying there is a second argument that’s not right. But in the tutorial we defined just one argument. what is this?
My function on the header is:
UFUNCTION()
void OnSensePlayer(TArray Actors);
UFUNCTION()
void OnSensePlayer(TArray Actors);
Your webpage sanitizer erases the bigger than, lesser than, star and AActor. it’s weird. Belive me, it has the same sognature as your property
Hello, this has been really helpful but I’m wondering about this part (note my project name is “EQS_QuickStart_CPP”):
if (Actor->IsA() && !GetSeeingPawn())
{
BlackboardComp->SetValueAsObject(BlackboardEnemyKey, Actor);
return;
}
As far as I can see, GetSeeingPawn() looks to see if there’s an AActor stored in the Blackboard Key, and returns that AActor, or nullptr if it isn’t an AActor there. So in the code above, we don’t set the key value if we’re already storing the Actor that triggers the perception update. In the line following, we then set the value to Null.
Doesn’t this mean that, if the same actor triggers the perception update twice in a row, the key value will be nullptr, when it should reference that actor?
I also wondered why we’re doing “Actor->IsA()”? Is that just the default player character in the third person template project?