What is the Environment Query System (EQS)?

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.
http://www.youtube.com/embed/bhPkA2klS6c?version=3&rel=1&fs=1&autohide=2&showsearch
=0&showinfo=1&iv_load_policy=1&wmode=transparent
Download The Source Code From My Github Repo
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 private:
2
3
/** BlackboardComponent - used to initialize blackboard values and set/get values from a blackboard
4 asset */
5
UBlackboardComponent* BlackboardComp;
6
7
/** BehaviorTreeComponent - used to start a behavior tree */
8
UBehaviorTreeComponent* BehaviorTreeComp;
9
10
/** Blackboard Key Value Name */
11
const FName BlackboardEnemyKey = FName("Enemy");
12
13
/** The function that fires when the perception of our AI gets updated */
14
UFUNCTION()
15
void OnPerceptionUpdated(TArray<AActor*> UpdatedActors);
16
17
/** A Sight Sense config for our AI */
18
UAISenseConfig_Sight* Sight;
19
20 protected:
21
22
/** The Behavior Tree that contains the logic of our AI */
23
UPROPERTY(EditAnywhere)
24
UBehaviorTree* BehaviorTree;
25
26
/** The Perception Component of our AI */
27
UPROPERTY(VisibleAnywhere)
28
UAIPerceptionComponent* AIPerceptionComponent;
29
30 public:
31
32
AMyAIController();
33
34
virtual void Possess(APawn* InPawn) override;
35
36
/** 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
void AMyAIController::OnPerceptionUpdated(TArray<AActor*> UpdatedActors)
{
//If our character exists inside the UpdatedActors array, register him
4
//to our blackboard component
5
6
for (AActor* Actor : UpdatedActors)
7
{
8
if (Actor->IsA<AEqsTutCharacter>() && !GetSeeingPawn())
9
{
10
BlackboardComp->SetValueAsObject(BlackboardEnemyKey, Actor);
11
return;
12
}
13
}
14
15
//The character doesn't exist in our updated actors - so make sure
16
//to delete any previous reference of him from the blackboard
17
18
19
20
21
22
23
BlackboardComp->SetValueAsObject(BlackboardEnemyKey, nullptr);
}
AMyAIController::AMyAIController()
{
//Components Init.
24
BehaviorTreeComp =
25 CreateDefaultSubobject<UBehaviorTreeComponent>(FName("BehaviorComp"));
26
27
BlackboardComp = CreateDefaultSubobject<UBlackboardComponent>(FName("BlackboardComp"));
28
29
AIPerceptionComponent =
30 CreateDefaultSubobject<UAIPerceptionComponent>(FName("PerceptionComp"));
31
32
//Create a Sight Sense
33
Sight = CreateDefaultSubobject<UAISenseConfig_Sight>(FName("Sight Config"));
34
35
Sight->SightRadius = 1000.f;
36
Sight->LoseSightRadius = 1500.f;
37
Sight->PeripheralVisionAngleDegrees = 130.f;
38
39
//Tell the sight sense to detect everything
40
Sight->DetectionByAffiliation.bDetectEnemies = true;
41
Sight->DetectionByAffiliation.bDetectFriendlies = true;
42
Sight->DetectionByAffiliation.bDetectNeutrals = true;
43
44
//Register the sight sense to our Perception Component
45
AIPerceptionComponent->ConfigureSense(*Sight);
46 }
47
48 void AMyAIController::Possess(APawn* InPawn)
49 {
50
Super::Possess(InPawn);
51
52
if (BehaviorTree)
53
{
54
//Initialize the Blackboard and start the attached behavior tree
55
BlackboardComp->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
56
BehaviorTreeComp->StartTree(*BehaviorTree);
57
}
58
59
//Register the OnPerceptionUpdated function to fire whenever the AIPerception get's updated
60
AIPerceptionComponent->OnPerceptionUpdated.AddDynamic(this,
61 &AMyAIController::OnPerceptionUpdated);
62 }
63
64 AActor* AMyAIController::GetSeeingPawn()
65 {
66
//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:
Click on image to enlarge in a new tab
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:
1. Click on the Edit menu
2. Open up the Editor Preferences
3. Select the Experimental tab
4. Check the Environment Query System
The following picture sums up the above steps:
Click on image to enlarge in a new tab
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:
Click on image to enlarge in a new tab
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:
1. Generators and
2. 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, FEnvQueryContex
tData& 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:
void UFindEnemyQueryContext::ProvideContext(FEnvQueryInstance& QueryInstance,
1 FEnvQueryContextData& ContextData) const
2 {
3
Super::ProvideContext(QueryInstance, ContextData);
4
5
//Get the Owner of this Query and cast it to an actor
6
//Then, get the actor's controller and cast to it our AIController
7
//This code works for our case but avoid that many casts and one-liners in cpp.
8
AMyAIController* AICon =
9 Cast<AMyAIController>((Cast<AActor>((QueryInstance.Owner).Get())->GetInstigatorController()));
10
11
if (AICon && AICon->GetSeeingPawn())
12
{
13
//Set the context SeeingPawn to the provided context data
14
UEnvQueryItemType_Actor::SetContextHelper(ContextData, AICon->GetSeeingPawn());
15
}
}
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:
Click on image to enlarge in a new tab
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.