RTree First Project Tutorial
For this tutorial, we are going to make a basic project with a single RTree-backed NPC. The player will be able to give tasks to the NPC via an on-screen text box. The NPC will be able to perceive numbered signs in the world and run to them in different orders depending on the player request.
This tutorial will showcase the basics of all major parts of RTree, including:
- The perception system
- How prompts are built and managed
- How to use a custom model
- How to extend Unreal Engine 5 Gameplay Abilities so they are useable by RTree
- How validation works
Setup
Project Creation
To start off, open the Unreal editor and create a new first person C++ project called, "RTreeTutorial". Include the starter content. You can leave the default for all other settings, no need for raytracing.
Once the project completes building, you should see something similar to the following:
Configure Unreal Editor For Rider
While Visual Studio has been the standard Unreal Engine C++ IDE for a while, Jet Brains' Rider provides a modern development environment with a drastically more efficient build process. However, everything in this tutorial still works with Visual Studio, so use that if you are more comfortable (and skip the steps below).
-
On the Edit menu on the top left of the Unreal Editor, select "Editor Preferences".
-
First we are going to disable Unreal Editor's automatic compilation features. You can leave this on if you prefer, but editing C++ in an IDE while these features are on can sometimes create issues that require rebuilding the entire project. We turn these off in two places, first we explicitly disable live coding. Search for it in the Editor Preference search bar and make sure
Enable Live Coding
is unchecked. -
Next, we are going to deactivate automatic compilation when we add new C++ classes. Combined with disabling live coding, this will make it easy for us to use Rider's excellent build system. Search for "automatically compile" in the Editor Preferences search bar and make sure
Automatically Compile Newly Added C++ Classes
underHot Reload
is unchecked. -
Next, we need to make sure that Rider is setup. First, under the
Plugins
settings (found in the same Edit menu as Editor Preferences or accessible from the gearbox icon above the top right corner of the editor viewscreen). Search for "rider". Make sure the Rider Integreation plugin is selected. -
After selecting the plugin, open up the Editor Preference menu once again and search for "source code". Under the
General - Source Code
,Accessor
drop down, make sure theSource Code Editor
attribute is set to "Rider Uproject".
Enable RTree Plugin
After downloading RTree from Fab, install it to the version of UE5 you are using and open the Plugins settings menu. From here, search for "RTree" and make sure the RTree plugin is enabled:
Configure Custom Model
Next, open the project settings in the editor and scroll down to RTree Settings. By default, RTree ships with qwen2.5-7b-instruct-1m
. If you wish to use an alternative .gguf
model, paste the filepath to the model here. Almost all .gguf
and (and the older .ggml
format) models should work, so long as they are supported by the version of llama.cpp
within this plugin. Currently, only text based models are supported. See below:
Note, updating the model will only affect which model is loaded for development. If you use a non-default model you will also need to make sure it is packaged by the final game build system (in addition to having its path set in these settings).
Project Build Configuration
Required Dependencies
- AIModule (UE5)
- Json (UE5)
- GameplayAbilities (UE5)
- GameplayTags (UE5)
- GameplayTasks (UE5)
RTree
Below, we see how the build configuration file should look once everything is included.
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class RTreeTutorial : ModuleRules
{
public RTreeTutorial(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput" }); // (1)!
PrivateDependencyModuleNames.AddRange(new string[] {
"AIModule", "RTree", "Json", "GameplayAbilities", "GameplayTags", "GameplayTasks" // (2)!
});
}
}
- These are default dependencies.
- These can be public or private dependencies depending on how your project is structured, however they are all required.
AIController Setup
The AI Controller is the meat of RTree. Before we talk about its implentation (don't worry, it's easy!), we should review the key concepts. From Epic's documentation:
Quote
Controllers are non-physical Actors that can possess a Pawn (or Pawn-derived class like Character) to control its actions. A PlayerController is used by human players to control Pawns, while an AIController implements the artificial intelligence for the Pawns they control. (source)
In order for RTree to control pawns, we need to:
- Subclass
ARTreeAIController
(instead of UE5's standard,AAIController
) - Implent
virtual FString ProcessObservations(TArray<RTreeObservation>& Observations) override
on the subclass. - For any Character/Pawn in the editor, set the subclassed controller class as the AI controller.
The easiest way to get started is to create a new class from the Unreal Editor. Click on the Tools menu at the top and select, "New C++ Class". First we need to choose the parent class.
Type "rtreeai" and we should see RTreeAIController
in the search results.
Select RTreeAIController
and click next. This will open the following menu:
We can name the class the RTreeTutorialAIController
and make it a public class. After clicking create class, we can open Rider and view our code.
// Copyright RTree, 2025
#pragma once
#include "CoreMinimal.h"
#include "RTreeAIController.h"
#include "RTreeTutorialAIController.generated.h"
/**
* A tutorial implementation of an RTree AI Controller.
* Responsible for processing environmental observations and tracking key locations.
*/
UCLASS()
class RTREETUTORIAL_API ARTreeTutorialAIController : public ARTreeAIController
{
GENERATED_BODY()
public:
virtual void BeginPlay();
FString ProcessObservations(TArray<RTreeObservation>& Observations) override;
protected:
UPROPERTY()
TMap<FString, FVector> DiscoveredLocations;
};
Before going into the implementation, it's important to understand what ProcessObservations()
does and how it works.
Perception System Overview
The RTree perception system consists of two main parts:
- Within the RTreeAIController, the delegate,
void ARTreeAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
is bound to the controller's UAIPerceptionComponent. Currently, only sight is supported.OnPerceptionUpdated
returns a list of all actors who triggered the perception system for update cycle (as opposed toOnTargetPerceptionUpdated
, which fires once for every actor.OnTargetPerceptionUpdated
is currently not supported). For a general overview of the UE perception system, this video is a good reference. For an overview of the perception system in C++, I like this video. - When Unreal Engine detects a new actor or actors in the configured sight radius, the delegate bound above is called with the array of detected actors. RTree then filters this list of actors to just those which have an RTreeDetectable component. This filtered list is then added to an observations cache, which we retrieve whenever we receive a player prompt.
We are almost ready to discuss the implementation for ARTreeTutorialAIController
, however there is one last part we need to discuss.
ProcessObservations Overview
We see an overview of the planning system here. The planning system is initiated when a player enters a new prompt. Critically, in our subclassed AI Controller, ARTreeTutorialAIController
, we must implement FString ARTreeD6AIController::ProcessObservations(TArray<RTreeObservation>& Observations)
. The purpose of this method is two-fold:
- Give developers control over how perceptions are rendered as text.
- Provide a point where developers have the option of saving perceptions into some sort of long-term memory.
We will now review the full implementation of ARTreeTutorialAIController
.
AIController Implementation Details
The implementation requirements for the ARTreeTutorialAIController
are very simple. Really, we only need to at a minimum implement virtual FString ProcessObservations(TArray<RTreeObservation>& Observations) override
. This is the function where we specify how we want the scene to be described to the model, and also where we might consider calling any sort of long-term memory system. Additionally, you will likely want to override the default SightConfig settings. For the tutorial, we're just using the same values as the defaults, but feel free to modify any of these.
// Copyright RTree, 2025
#include "RTreeTutorialAIController.h"
#include "Perception/AIPerceptionComponent.h" // (1)!
void ARTreeTutorialAIController::BeginPlay()
{
Super::BeginPlay();
DiscoveredLocations = TMap<FString, FVector>();
UAIPerceptionComponent* PerceptionComp = GetPerceptionComponent(); // Get the actual component
if (!PerceptionComp)
{
return;
}
if (SightConfig) // (2)!
{
SightConfig->SightRadius = RTreeSightRadius;
SightConfig->LoseSightRadius = SightConfig->SightRadius + 25.f;
SightConfig->PeripheralVisionAngleDegrees = 360.f;
SightConfig->SetMaxAge(30.f);
SightConfig->AutoSuccessRangeFromLastSeenLocation = 520.f;
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
PerceptionComp->SetDominantSense(*SightConfig->GetSenseImplementation());
PerceptionComp->ConfigureSense((*SightConfig));
}
}
FString ARTreeTutorialAIController::ProcessObservations(TArray<RTreeObservation>& Observations)
{
FString ObservationPrompt = "[Start of Observation] You perceive the following:\n"; // (3)!
for (RTreeObservation& obs : Observations)
{
DiscoveredLocations.Emplace(obs.GetEnumName(), obs.GetMovementLocation()); // (4)!
ObservationPrompt += "Entity description: ";
ObservationPrompt += obs.GetVisualDescription();
ObservationPrompt += "\n";
}
ObservationPrompt += "[End of Observation]\n"; // (5)!
return ObservationPrompt;
}
- Be sure to include this if you want to modify the default sight configuration below (needs
UAIPerceptionComponent
). - Below we configure the SightConfig. The values below are the defaults set in the parent
RTreeAIController
, however you may override any of those here. - Instructions letting the model know this is a description of perception are generally helpful. Make sure to have one at the start and the end of the perception part of the prompt.
- In this tutorial project, we are saving known locations to a map, however in reality these could be stored in more complex, custom systems memory systems (such as a vector database).
- Instructions letting the model know this is a description of perception are generally helpful. Make sure to have one at the start and the end of the perception part of the prompt.
Now we've set up our AI Controller, but how do we define the actions that an RTree-controlled actor can take? For this, we rely on the Unreal Engine's Gameplay Ability System.
Gameplay Ability System (GAS) Overview
RTree utilizes Unreal Engine's excellent Gameplay Ability System to define and manage actions models can take in plans. The basic workflow for GAS is:
graph LR
GW["Game World"]
GAS["Gameplay Ability System"]
M["Model"]
GAS -- "JSON ability schemas" --> M
M -- "JSON ability function calls" --> GAS
GAS -- "Unreal API calls" --> GW
GW -- "Contextually up-to-date validation params & executed ability status" --> GAS
classDef default fill:#2692D940,stroke:#2692D9,stroke-width:2px,color:#000
class GW,GAS,M default
In RTree, GAS is responsible for:
- Defining abilities both in terms of how they run according to the UE5 API, and their JSON schema so they may be called by a model.
- Validating if an action can be activated according to in-game criteria.
- Keeping the JSON validation schema of an action up-to-date according to game state. For example, a GoTo action might maintain a dynamic list of locations which it can be called with which changes as more locations are discovered in the game.
- Validating the arguments an action is called with by a model. Specifically, this is the logic that, for example, checks if a passed location enum is within the list of currently valid locations.
- Tracking the status of a running action. Is the action still in progress? Did the action succeed? Did the action fail?
We can summarize these concepts in the following table:
Description | GAS as it relates to game world | GAS as it relates to model |
---|---|---|
How is an action represented? | Actions are represented as a sequence of UE5 API calls | Actions are represented as a JSON schema |
How is contextually up-to-date schema handled? | Pre-inference, generates the parameters that a model should be able to call an action with, for the requisite character, at the current point-in-time in the game world. | The contextually up-to-date schema (and validation requirements) generated from the Game World are rendered into the prompt as JSON-7 schemas. |
How is success tracked? | Did the action the model called succeed in-game? | Did the model call a valid schema? |
Epic has done a great job of designing a generic, performant and highly functional context management system. That being said, there is an inherent amount of complexity and as such before jumping into the code, it makes sense to review key terms.
GAS key terms
- Attributes - Stored as a float and modified by gameplay effects.
- Attrribute Sets - Contain 1+ attributes and can define logic within the attribute set that manage how attributes within the set or modified.
- Gameplay Tags - Not exclusively for gameplay ability system but the GAS makes heay use of them.
- Gameplay Effects - Usually defined in blueprints. Defines interactions with attributes, including requirements, durations and can add/remove gameplay tags and gameplay cues. Can also define stacking rules (how many stacks of effect are permitted). Can also grant abilities.
- Ability tasks - These are the building blocks for abilities. They are based off of UE5's gameplay tasks, giving them async execution. These exist in C++ and Blueprint and deinfe delegates which control the execution flow within a gameplay ability. They are explicitly tied to the lifecycle of the gameplay abilty.
- Gameplay Abilities - Can be blueprint or C++. Made up of ability tasks. Each ability can define its own requirements for when to run. Can also have cooldowns.
- Ability System Component - Actor component, contains abilities and attributes. Used as a target for interactions.
For this tutorial we will create an Ability System component, a single, "GoTo" Gameplay Ability and a "GoTo" Ability Task. These will all be tied together via our character class.
Why is the ability system attached to the instance of the character and not the AIController, for example like the perception component?
We can think of the character class as the body of a character. Some of the things the character class handles include physical movement and physics simulation, mesh and animation representation, collision detection, heath, stats and attributes, and abilities and actions the character can perform. These responsibilities would be the same whether a character is controlled by an AI or a person.
In contrast, the AIController represents the "brain" that controls a character. Included in the things it handles are: decision-making logic, navigation and pathfinding, target selection and prioritization, AI perception and sensing. Perception is about decision making, not physical capabilities. We can imagine a scenario where a single character could be perceived and reacted to differently by an AllyAIController vs EnemyAIController. Maybe the ally AI controller can perceive more stats or current inventory, for example.
The gameplay ability system (GAS) is tied to the character because abilities are intrinsic properties of the character itself. Whether controlled by a player or AI, the character has the same set of abilities, cooldowns and effects.
What about the situation where a character is blinded due to the effect of an attack? Shouldn't this effect the AIController-associated perception system?
- The Gameplay Ability System would apply the "blind" gameplay effect to the character.
- The effect would modify an attribute like "VisionRange" or add a "Blind" gameplay tag
- This lives on the character because it's a status effect affecting the character's capabilities
- The AIController queries the character for perception modifiers.
- The AIController's perception component checks the character's ability system component for relevant tags like "Status.Blind" or other attributes affecting perception.
- The perception component would then adjust its sensing parameters accordingly.
For this tutorial, we will implement a single gameplay ability which will allow an NPC character to move to a discovered location. When activated, this "GoTo" ability will initialize and activate an ability task, which is where the Unreal movement API is actually called.
The basic workflow is:
- Plan (ordered list of JSON function calls) generated by model.
- For each step in plan, we validate that the JSON call supplied by the model is valid according to the schema supplied to the model in the prompt.
- If the JSON call is valid, we attach it to a struct which includes the requisite
FGamePlayAbilitySpec
, along with the model-supplied parameters and enqueue the struct. - Once all of the steps have been enqueued, we tell the owning controller to start the plan.
- The owning controller dequeues the struct at the front of the plan queue
- The owning controller maintains a pointer to its controlled character's Ability System Component, and calls
TryActivateAbility(PlanStep->AbilitySpec.Handle)
, which checks to see if the ability can be activated first, and then (under the hood) calls the actual ability'sActivateAbility()
method, which we implement for each custom ability.
Ability System Component
Now that we have the basics covered, we need to setup the Gameplay Ability System in our codebase. Go to "Tools" -> "New C++ Class...". On the switch at the top of the Add C++ Class menu, select, "All Classes". In the search bar, type "abilitysystemcomponent" and select the requisite class.
Click "Next". We will keep the class public and name it, "RTreeAbilitySystemComponent".
Before creating the class, modify the path so we store this in a directory called AbilitySystem/
, within our existing Source/RTreeTutorial/
directory. See screenshot below for example:
Once you've confirmed the path is good, click "Create Class".
Switch back to the IDE and open RTreeAbilitySystemComponent.h
and RTreeAbilitySystemComponent.cpp
.
In the header, under the GENERATED_BODY()
macro, add:
In total, the header file should now look like:
// Copyright RTree, 2025
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemComponent.h"
#include "RTreeAbilitySystemComponent.generated.h"
/**
*
*/
UCLASS()
class RTREETUTORIAL_API URTreeAbilitySystemComponent : public UAbilitySystemComponent
{
GENERATED_BODY()
public:
void AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities);
};
Next switch to the implementation file and add:
void URTreeAbilitySystemComponent::AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities)
{
for (TSubclassOf<UGameplayAbility> AbilityClass : StartupAbilities)
{
// to grant an ability, must create an ability spec first
const FGameplayAbilitySpec AbilitySpec = FGameplayAbilitySpec(AbilityClass, 1);
GiveAbility(AbilitySpec); // (1)!
}
}
- This grants an ability to a character but does not activate it. For this demo we grant abilities on startup, but in an actual game you might grant/revoke abilities at any point.
A short digression - configuring logging
Before continuing on with our tutorial, we should configure a basic logging strategy that will allow us to debug and view log messages specifically from code we implement in this tutorial. While there are a variety of ways to accomplish this, we will create a basic macro which will
in turn call UE5's UE_LOGFMT
.
In Rider, within the file explorer, right-click on Source/RTreeTutorial/Public/
and select Add->File. Name the file RTreeTutLogging.h
.
Once the newly created file is open, add the following:
#pragma once
#include "Logging/LogMacros.h"
#include "Logging/StructuredLog.h"
DECLARE_LOG_CATEGORY_EXTERN(LogRTreeTut, Log, All);
#define RTREETUT_LOGFMT(Verbosity, Format, ...) \
UE_LOGFMT(LogRTreeTut, Verbosity, "{0}: " Format, TEXT(__FUNCTION__), ##__VA_ARGS__)
We need to create an implementation file to call DEFINE_LOG_CATEGORY
. Follow the above to create another new file except in Source/RTreeTutorial/Private/
. Name this file RTreeTutLogging.cpp
.
Inside the implementation file, add:
So long as we import RTreeTutLogging.h
, we can easily call UE_LOGFMT
without having to repeatedly type in LogRTreeTut
and we automatically get the name of the exact function we log from. As we will see when we implement it further along in the tutorial, RTREETUT_LOGFMT(Log, "Activating GoTo ability task");
will output in the logs as LogRTreeTut: UGoToAbilityTask::Activate: Activating GoTo ability task
. Since UE_LOGFMT
supports variadic arguments, we accept them here as well, which enables us to easily pass messages in args, for example RTREETUT_LOGFMT(Log, "Preparing to move to the destination {1}." Destination.ToCompactString());
which in turn renders as, LogRTreeD6: URTreeD6GoToGameplayAbility::ActivateAbility: Preparing to move to the destination V(X=485.00, Y=2643.00, Z=160.00).
. One caveat is that we need to use 1-based indexing for the positional args rather than the standard 0-based indexing becasue the function name is actually the 0th arg passed.
The Gameplay Ability Task Handles the Action
Now that we have the ability system component and a basic logging system, we will create a gameplay ability task. We can think of the ability task as the thing that handles the literal action of a gameplay ability and reports its status back to the owning ability. An ability can have one or more tasks.
To get started, in the Unreal Editor select "Tools->New C++ Class..." Search for "abilitytask" and select Ability Task (class source should be AbilityTask.h
). Click "Next" and make the class public. Name the class GoToAbilityTask
. We want to keep this path in a new directory within AbilitySystem
, specifically AbilitySystem/AbilityTasks/
. See the screenshot below, but the path should look something like: C:/Users{your_username}/UE5/RTreeTutorial/Source/RTreeTutorial/Public/AbilitySystem/AbilityTasks/
.
Click "Create Class".
The freshly created class should look like:
#pragma once
#include "CoreMinimal.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "GoToAbilityTask.generated.h"
/**
*
*/
UCLASS()
class RTREETUTORIAL_API UGoToAbilityTask : public UAbilityTask
{
GENERATED_BODY()
};
Let's update GoToAbilityTask.h
to look like:
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "Abilities/Tasks/AbilityTask.h"
#include "Navigation/PathFollowingComponent.h"
#include "GoToAbilityTask.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGoToAbilityTaskDelegate); // (1)!
/**
* UGoToAbilityTask is a gameplay ability task responsible for handling
* character movement to a specified target destination. This task utilizes
* pathfinding capabilities, and provides various callbacks for success, failure,
* or interruptions.
*/
UCLASS()
class RTREETUTORIAL_API UGoToAbilityTask : public UAbilityTask
{
GENERATED_BODY()
public:
static UGoToAbilityTask* CreateGoToTask( // (2)!
UGameplayAbility* OwningAbility,
FName TaskInstanceName,
FVector TargetDestination);
virtual void Activate() override;
private:
virtual void OnDestroy(bool bInOwnerFinished) override;
void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result);
FVector Destination;
FAIRequestID MoveRequestID;
UPROPERTY()
AAIController* CachedAIController;
public:
// Called when the task finishes successfully (character reached target)
FGoToAbilityTaskDelegate OnCompleted; // (3)!
// Called when movement fails (unable to find path or canโt move)
FGoToAbilityTaskDelegate OnFailed; // (4)!
// Called when the task is interrupted (ability canceled or otherwise)
FGoToAbilityTaskDelegate OnInterrupted; // (5)!
};
- The gameplay ability system requires dynamic multicast delegates. Dynamic is required as we instantiate ability tasks and thus need to bind delegates at runtime. Multicast is because, while we are not using it in this tutorial, the gameplay ability and ability task systems are also suported in Blueprint, which means a delegate could fire and need to notify Blueprint and C++ functions simultaneously.
- Note the static constructor which is what we will call from the ability when instantiating/configuring the ability task.
- In the implementation, we will call
.Broadcast()
on these delegates depending on certain criteria. - In the implementation, we will call
.Broadcast()
on these delegates depending on certain criteria. - In the implementation, we will call
.Broadcast()
on these delegates depending on certain criteria.
Next, we need to create the implementation. Open GoToAbilityTask.cpp
.
#include "AbilitySystem/AbilityTasks/GoToAbilityTask.h"
#include "RTreeCharacter.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "RTreeTutLogging.h"
UGoToAbilityTask* UGoToAbilityTask::CreateGoToTask(UGameplayAbility* OwningAbility, FName TaskInstanceName, FVector TargetDestination)
{
UGoToAbilityTask* GoToTask = NewAbilityTask<UGoToAbilityTask>(OwningAbility, TaskInstanceName);
GoToTask->Destination = TargetDestination;
return GoToTask;
}
void UGoToAbilityTask::Activate()
{
Super::Activate();
AActor* AvatarActor = GetAvatarActor();
if (!AvatarActor)
{
EndTask();
OnFailed.Broadcast();
return;
}
RTREETUT_LOGFMT(Log, "Activating GoTo ability task");
ARTreeCharacter* Character = Cast<ARTreeCharacter>(AvatarActor);
if (Character)
{
if (UCharacterMovementComponent* MovementComponent = Character->GetCharacterMovement())
{
RTREETUT_LOGFMT(Log, "Acquired movement component");
MovementComponent->SetMovementMode(EMovementMode::MOVE_NavWalking);
MovementComponent->MaxWalkSpeed = 600.f;
}
else
{
RTREETUT_LOGFMT(Log, "Failed to acquire movement component");
}
CachedAIController = Cast<AAIController>(Character->GetController());
}
if (!CachedAIController)
{
RTREETUT_LOGFMT(Error, "No CachedAIController");
EndTask();
OnFailed.Broadcast();
return;
}
FAIMoveRequest MoveReq;
MoveReq.SetGoalLocation(Destination);
MoveReq.SetAcceptanceRadius(5.0f);
MoveReq.SetNavigationFilter(nullptr);
MoveReq.SetAllowPartialPath(true);
RTREETUT_LOGFMT(Log, "Initiating Move.");
// initiate the move
MoveRequestID = CachedAIController->MoveTo(MoveReq);
RTREETUT_LOGFMT(Log, "GoToAbilityTask activated with ID: {1}", MoveRequestID.GetID());
if (CachedAIController->GetPathFollowingComponent())
{
CachedAIController->GetPathFollowingComponent()->OnRequestFinished.AddUObject(
this, &UGoToAbilityTask::OnMoveCompleted
);
RTREETUT_LOGFMT(Log, "PathFollowingComponent->OnRequestFinished allocated size = `{1}`", CachedAIController->GetPathFollowingComponent()->OnRequestFinished.GetAllocatedSize());
}
else
{
RTREETUT_LOGFMT(Error, "CachedAIController->GetPathFollowingComponent() returned nullptr.");
EndTask();
OnFailed.Broadcast();
}
}
void UGoToAbilityTask::OnDestroy(bool bInOwnerFinished)
{
// If we need to abort movement manually
if (CachedAIController && MoveRequestID.IsValid())
{
CachedAIController->StopMovement();
// Make sure we stop listening for any leftover move results
if (CachedAIController->GetPathFollowingComponent())
{
CachedAIController->GetPathFollowingComponent()->OnRequestFinished.RemoveAll(this);
}
}
Super::OnDestroy(bInOwnerFinished);
}
void UGoToAbilityTask::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
RTREETUT_LOGFMT(Log, "OnMoveCompleted called Result is : {1}", Result.ToString());
// Unbind since we got our callback
if (CachedAIController && CachedAIController->GetPathFollowingComponent())
{
CachedAIController->GetPathFollowingComponent()->OnRequestFinished.RemoveAll(this);
}
RTREETUT_LOGFMT(Log, "Result.Code is {1}", Result.Code);
EndTask();
// Check the result
switch (Result.Code)
{
case EPathFollowingResult::Success:
RTREETUT_LOGFMT(Log, "GoToAbilityTask move successful.");
OnCompleted.Broadcast();
break;
case EPathFollowingResult::Aborted:
OnInterrupted.Broadcast();
break;
case EPathFollowingResult::Blocked:
case EPathFollowingResult::OffPath:
case EPathFollowingResult::Invalid:
default:
OnFailed.Broadcast();
break;
}
}
The Gameplay Ability Handles Validation and Calls the Gameplay Task
At this point, we've implemented the Ability System Component and an ability task. Now it's time to create the actual gameplay ability which is managed by the Ability System Component and instantiates/calls the ability task.
To get started, in the Unreal Editor, select Tools->New C++ Class.... Search for "gameplayability" and select the generic Gameplay Ability class (class source should be GameplayAbility.h
). Click "Next" and make the class public. Name the class RTreeGoToGameplayAbility
. We want to keep this path in a new directory within AbilitySystem
, specifically the AbilitySystem/Abilities/
directory, so the path should look something like: C:/Users/{your_username}/UE5/Games/RTreeTutorial/Source/RTreeTutorial/Public/AbilitySystem/Abilities/
.
Click "Create Class".
Our freshly created class (RTreeGoToGameplayAbility.h
) should look like:
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "RTreeGoToGameplayAbility.generated.h"
/**
*
*/
UCLASS()
class RTREETUTORIAL_API URTreeGoToGameplayAbility : public UGameplayAbility
{
GENERATED_BODY()
};
Update, so the the header looks like:
#pragma once
#include "CoreMinimal.h"
#include "RTreeAbilityInterface.h" // (1)!
#include "Abilities/GameplayAbility.h"
#include "RTreeGoToGameplayAbility.generated.h"
/** Class responsible for validating movement ability calls from model and executing in-game
*/
UCLASS()
class RTREETUTORIAL_API URTreeGoToGameplayAbility : public UGameplayAbility, public IRTreeAbilityInterface // (2)!
{
GENERATED_BODY()
public:
virtual void ActivateAbility( // (3)!
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
// IRTreeAbilityInterface start
TSharedRef<FJsonObject> GetValidationSchema(AActor* Actor); // (4)!
// IRTreeAbilityInterface end
private:
UFUNCTION()
void OnCancelled();
UFUNCTION()
void OnCompleted();
UFUNCTION()
void OnFailed();
};
- Make sure to add this!
- We also have to inherit from
IRTreeAbilityInterface
so RTree knows to use this ability. - This is where we will define what the ability does.
- This is where we build the schema that tells the model how to call the ability.
Next, open the implementation file, RTreeGoToGameplayAbility.cpp
.
#include "AbilitySystem/RTreeGoToGameplayAbility.h"
#include"AbilitySystem/AbilityTasks/GoToAbilityTask.h"
#include "RTreeTutorialAIController.h"
#include "RTreeTutLogging.h"
void URTreeGoToGameplayAbility::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// Get the avatar actor from the ActorInfo
AActor* AvatarActor = ActorInfo->AvatarActor.Get();
if (!AvatarActor)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// Cast the avatar to APawn since we need it to get the controller
APawn* Pawn = Cast<APawn>(AvatarActor);
if (!Pawn)
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
// Get and cast to the AI Controller
ARTreeTutorialAIController* AIController = Cast<ARTreeTutorialAIController>(Pawn->GetController());
if (!AIController)
{
// The pawn either has no controller or it's not an AI Controller
RTREETUT_LOGFMT(Error, "AIController is null when attempting to execute GoTo ability.");
OnFailed();
return;
}
// Dequeue the function call
TSharedPtr<FJsonObject> ValidatedCallJsonObj = AIController->GetCurrentPlanStepJson();
if (ValidatedCallJsonObj == nullptr)
{
RTREETUT_LOGFMT(Error, "FJsonObject is null when attempting to execute GoTo ability.");
OnFailed();
return;
}
TSharedPtr<FJsonValue> DestinationName = ValidatedCallJsonObj->TryGetField("Destination");
if (!DestinationName)
{
RTREETUT_LOGFMT(Error, "Required Destination name not found in supplied validated call.");
OnFailed();
return;
}
if (const FVector* DestinationVecPtr = AIController->DiscoveredLocations.Find(DestinationName->AsString()))
{
FVector Destination = *DestinationVecPtr;
RTREETUT_LOGFMT(Log, "Preparing to move to the destination {1}.", Destination.ToCompactString());
UGoToAbilityTask* Task = UGoToAbilityTask::CreateGoToTask(this, "MoveTo", Destination);
Task->OnCompleted.AddDynamic(this, &URTreeGoToGameplayAbility::OnCompleted);
Task->OnFailed.AddDynamic(this, &URTreeGoToGameplayAbility::OnFailed);
Task->OnInterrupted.AddDynamic(this, &URTreeGoToGameplayAbility::OnCancelled);
Task->ReadyForActivation();
}
else
{
RTREETUT_LOGFMT(Error, "Called Destination not in Actor's DiscoveredLocations TMap.");
OnFailed();
return;
}
}
/*
GetValidationSchema will produce an FJsonObject identical or similar to:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MoveTo",
"description": "Schema with valid locations for move to action.",
"type": "object",
"properties": {
"destination": {
"description": "Available destinations to move to.",
"enum": [
"SignNum3",
"SignNum1",
"SignNum5",
"SignNum4",
"SignNum2"
]
},
"title": {
"description": "MoveTo is the name of this function.",
"title": "MoveTo"
}
},
"required": [
"title",
"destination"
]
}
*/
TSharedRef<FJsonObject> URTreeGoToGameplayAbility::GetValidationSchema(AActor* Actor)
{
RTREETUT_LOGFMT(Log, "Getting validation schema...");
TSharedRef<FJsonObject> Schema = MakeShared<FJsonObject>();
ARTreeTutorialAIController* OwningController = Cast<ARTreeTutorialAIController>(Actor);
if (OwningController == nullptr)
{
RTREETUT_LOGFMT(Fatal, "Cast to RTreeTutorialAIController failed!");
return Schema;
}
TArray<FString> AvailableDestinations;
RTREETUT_LOGFMT(Log, "Number of items in DiscoveredLocations: {1}", OwningController->DiscoveredLocations.Num());
OwningController->DiscoveredLocations.GenerateKeyArray(AvailableDestinations);
if (AvailableDestinations.Num() == 0)
{
RTREETUT_LOGFMT(Warning, "No available destinations.");
return Schema;
}
Schema->SetStringField("$schema", "http://json-schema.org/draft-07/schema#");
Schema->SetStringField("title", "MoveTo");
Schema->SetStringField("description", "Schema with valid locations for move to action.");
Schema->SetStringField("type", "object");
TSharedPtr<FJsonObject> PropertiesObj = MakeShared<FJsonObject>();
TSharedPtr<FJsonObject> DestinationObj = MakeShared<FJsonObject>();
TSharedPtr<FJsonObject> TitleObj = MakeShared<FJsonObject>();
DestinationObj->SetStringField("description", "Available destinations to move to.");
TitleObj->SetStringField("description", "MoveTo is the name of this function.");
TArray<TSharedPtr<FJsonValue>> EnumValues;
for (auto& dest : AvailableDestinations)
{
EnumValues.Add(MakeShared<FJsonValueString>(dest));
}
DestinationObj->SetArrayField("enum", EnumValues);
TitleObj->SetStringField("title", "MoveTo");
PropertiesObj->SetObjectField("destination", DestinationObj);
PropertiesObj->SetObjectField("title", TitleObj);
Schema->SetObjectField("properties", PropertiesObj);
TArray<TSharedPtr<FJsonValue>> RequiredFields;
RequiredFields.Add(MakeShared<FJsonValueString>("title"));
RequiredFields.Add(MakeShared<FJsonValueString>("destination"));
Schema->SetArrayField("required", RequiredFields);
return Schema;
}
void URTreeGoToGameplayAbility::OnCancelled()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
void URTreeGoToGameplayAbility::OnFailed()
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
void URTreeGoToGameplayAbility::OnCompleted()
{
RTREETUT_LOGFMT(Log, "Completed GoTo ability.");
// Get the avatar actor from the ActorInfo
AActor* AvatarActor = GetOwningActorFromActorInfo();
if (!AvatarActor)
{
RTREETUT_LOGFMT(Error, "AvatarActor is null.");
return;
}
// Cast the avatar to APawn since we need it to get the controller
APawn* Pawn = Cast<APawn>(AvatarActor);
if (!Pawn)
{
RTREETUT_LOGFMT(Error, "Pawn is null.");
return;
}
// Get and cast to the AI Controller
ARTreeTutorialAIController* AIController = Cast<ARTreeTutorialAIController>(Pawn->GetController());
if (!AIController)
{
// The pawn either has no controller or it's not an AI Controller
RTREETUT_LOGFMT(Error, "AIController is null.");
return;
}
AIController->HandleOnPlanStepComplete();
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
Each gameplay ability's GetValidationSchema
method must return a valid schema validator so that RTree can correctly parse and interpret the model output.
To confirm that your schema validator json is correctly structured, you can use the following meta schema validator and a tool like jsonschemavalidator.net to validate the json produced by your GetValidationSchema
method.
RTree Meta Schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Meta-Schema for MoveTo-like Schemas",
"description": "This schema validates that a given JSON schema has the minimum required structure of the 'MoveTo' schema.",
"type": "object",
"properties": {
"$schema": {
"description": "The JSON Schema version identifier. Must be a URI.",
"type": "string",
"format": "uri"
},
"title": {
"description": "The title of the schema.",
"type": "string"
},
"description": {
"description": "A brief explanation of the schema's purpose.",
"type": "string"
},
"type": {
"description": "The root type of the schema. For this structure, it must always be 'object'.",
"type": "string",
"const": "object"
},
"properties": {
"description": "The properties definition of the schema being validated.",
"type": "object",
"properties": {
"destination": {
"description": "Must contain a 'destination' property object.",
"type": "object",
"properties": {
"description": {
"type": "string"
},
"enum": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"description",
"enum"
]
},
"title": {
"description": "Must contain a 'title' property object.",
"type": "object",
"properties": {
"description": {
"type": "string"
},
"title": {
"type": "string"
}
},
"required": [
"description",
"title"
]
}
},
"required": [
"destination",
"title"
]
},
"required": {
"description": "An array listing the required properties for the schema.",
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"$schema",
"title",
"description",
"type",
"properties",
"required"
]
}
Now we have the barebone basic GAS components needed for the tutorial project, next we add these to our character.
RTree Character Setup
From the Unreal Engine editor, from the Tools menu select "New C++ Class...". For the partent class, select "Character" and click next. On the subsequent page, name the class "RTreeCharacter".
Click "Create Class" and return to the IDE.
Open RTreeCharacter.h
which, if you created the character as shown in the screenshot above, should be located at: RTreeTutorial/Source/Public/RTreeCharacter.h
.
By default, the project will already have RTreeTutorialCharacter.h
. This is not the class we are going to use. Make sure you are editing RTreeCharacter
and not RTreeTutorialCharacter
.
The freshly-created class should look like:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "RTreeCharacter.generated.h"
UCLASS()
class RTREETUTORIAL_API ARTreeCharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
ARTreeCharacter();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
};
Let's update this class to suit our needs.
- We need to add the import,
#include "AbilitySystemInterface.h"
for the GAS. Also, make sure to add a second inheritance class,public IAbilitySystemInterface
. - We can remove the entire second
public
block, as we won't be usingTick()
orSetupPlayerInputComponent()
. -
In the
protected
section add:virtual void PossessedBy(AController* NewController) override;
Per Epic's C++ style guide, when overriding a method, we use bothvirtual
andoverride
keywords.void AddCharacterAbilities();
-
Add a
private
section at the bottom of the class and add:
In total, we should now have:
#pragma once
#include "CoreMinimal.h"
#include "AbilitySystemInterface.h"
#include "GameFramework/Character.h"
#include "RTreeCharacter.generated.h"
class UAbilitySystemComponent;
class UGameplayAbility;
class UAttributeSet;
UCLASS()
class RTREETUTORIAL_API ARTreeCharacter : public ACharacter, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
ARTreeCharacter();
virtual UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
virtual void BeginPlay() override;
virtual void PossessedBy(AController* NewController) override;
UPROPERTY()
TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
void AddCharacterAbilities();
private:
UPROPERTY(EditAnywhere, Category="Abilities")
TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
};
For the implementation file, open Private\RTreeCharacter.cpp
and update it to:
#include "RTreeCharacter.h"
#include "AbilitySystem/RTreeAbilitySystemComponent.h"
ARTreeCharacter::ARTreeCharacter()
{
PrimaryActorTick.bCanEverTick = false;
AbilitySystemComponent = CreateDefaultSubobject<URTreeAbilitySystemComponent>("AbilitySystemComponent");
}
UAbilitySystemComponent* ARTreeCharacter::GetAbilitySystemComponent() const
{
return AbilitySystemComponent;
}
void ARTreeCharacter::BeginPlay()
{
Super::BeginPlay();
AbilitySystemComponent->InitAbilityActorInfo(this, this);
}
void ARTreeCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
AddCharacterAbilities();
}
void ARTreeCharacter::AddCharacterAbilities()
{
URTreeAbilitySystemComponent* RTreeASC = CastChecked<URTreeAbilitySystemComponent>(AbilitySystemComponent);
RTreeASC->AddCharacterAbilities(StartupAbilities);
}
Returning to the Unreal Editor and clearing scene
Now that most of our code is configured, it's time to return to the editor and set up the actual demo. Before launching the editor, we need to compile all of the code we've made. In Rider, the easiest way to compile the latest saved code and immediately open the Unreal Editor is to click on the play icon in the top right of the Rider IDE:
Once compilation is complete (you may have some typos to you need to fix before compilation runs successfully - keep working through the changes and clicking on the same play icon each time until compilation successfully completes), the editor will load and you should see the following:
Right click on the mouse and, using the wasd keys, move the camera up so you can see the entire scene like the below:
For this demo, we are going to level out everything here, however feel free to come back after we get the basics running and make any geometry changes you want. Select and delete the large grey cubes or small blue boxes (technically cubes as well). At the end, you should have roughly this:
Making the signs
For the demo, there will be a series of numbered signs that our RTree-driven NPC will be able to perceive and reason about. To set this up, we need to prepare our first sign. From the menu at the top of the editor, click on the icon with a cube and plus sign. Then, in the drop-down, hover over "Shapes" and on the final menu to the right, select the "Cube" option. See the screenshot:
Next, move the cube to the desired spot in the scene and reshape it to look a bit like a sign or flag. Manipulating geometry in Unreal Engine Editor is out of the scope for this tutorial, but there are many tutorials online which cover the very basics in 10-15 minutes.
Next, we want to add a text renderer to the static mesh component of the cube so we can visually display the content on the sign. On the Details tab for the selected cube, click add and select the Text Renderer.
To make life easier as we configure the text renderer, type in "hi there" into the text box for the text renderer component we just added. we will likely need to reposition, rotate, resize and recolor the text we just added.
Finally, we should have the text facing the correct direction and centered on our sign. Change the text to "1".
The preception system in RTree doesn't process visual data. Instead, it gets metadata about all of the actors it should perceive. Next, we need to add an RTreeDetectable
component to our sign and fill out the requisite metadata.
While the same cube is selected, select the Add button on the Details tab and search for "rtree". Select RTreeDetectable
.
Once the detectable is added, we need to configure it. Select the newly added RTreeDetectable
component and for visual description add, "Sign with number "1" on it". For the RTree Name, let's call it "SignNum1". The description and name are what will be submitted to the model as part of the perception and planning loop. We will come back and update the Movement Location
attribute once we have all of our signs made and configured.
Lastly, we need to add an AIPerceptionStimuliSource
component to the actor. This is necessary so that Unreal's perception system will pickup the actor. Click on Add in the Cube details tab and search "AIPerception". Select the AIPerceptionStimuliSource component.
To configure the component, select the checkbox, "Auto Register as Source" and under the "Register as Source for Senses" array, add "AISense_Sight". See the screenshot:
For this tutorial, we will have 5 signs numbered 1-5. So, let's copy the sign (and sign post) four times. There are several ways to do this but a basic easy way is just to select the sign and its post in the outliner and control-c/control-v. After pasting make sure to pull the sign off the top of the existing sign.
Next, let's update the content on the sign and in its RTreeDetectable
component. Note, we are still not updating the movement location attribute.
After updating the sign text and its RTreeDetectable
compliment, let's orient the signs in a way so they face towards the corner.
At this point, we are ready to update the movement location attribute. The movement location is the xyz position a character would move to should they go to any actor with an RTreeDetectable
component. To get these values, we will move the signs slightly toward the corner they face, copy their xyz location, move them back and paste the former xyz location as the movement location attribute.
Once you are complete, save the changes in the editor. In the next section we will add and configure an RTree-backed NPC character.
Create Blueprint version of AI Controller
Before we can continue, we need to create a blueprint version of our RTreeTutorialAIController
. Blueprints serve a variety of purporses in terms of being a main interface for game designers. In our case, creating blueprints allows us to configure instance-level defaults in the editor. In otherwords, anything we drag onto our level which has a 3D representation, we drag the blueprint version of the C++ class so we can configure instance-level details for that specific actor we dragged into the screen.
Let's make a directory in the content browser to store our blueprint class in. Right click on the Content/
directory and make a new folder called, Blueprints/
.
Next, go to C++Classes/RTreeTutorial/Public and right click on RTreeTutorialAIController
. Select the create Blueprint class option
Name the class BP_RTreeTutorialAIController
and make sure it is in our newly-created Content/Blueprints/
folder.
Open the blueprint and add the AI Sight config to the senses config index. Se AISense_Sight to the dominant sense.
Next, we want to set the RTree sight radius so we can perceive the entire environment. Search "rtree" and make sure the RTree Sight Radius is set to 3000.
Configure an NPC Character
Now we need to create a blueprint class out of the RTreeCharacter
class we created earlier in C++. This will allow us to easily configure the class within the editor. Recall that the purpose of the RTreeCharacter class is to store the ability system component and to register with its RTreeAIContoller on spawn.
Next, navigate to the RTreeCharacter
class in the "C++ Classes" directory. Remember we are looking for the RTreeCharacter
class and not the RTreeTutorialCharacter
class. See screenshot:
Let's name our new class BP_RTreeCharacter
and put it in Content/Blueprints/
.
After creating the class, you should see the following blueprint editor:
We need to add assets for the skeleton mesh and its animations that we will be using for the RTreeCharacter. By default, these are not in the first person project we created. To access these we will add the Third Person content pack to the project.
Select the ThirdPerson content on the Blueprint tab.
Now, we can select the mesh and animation class that will be used for BP_RTreeCharacter
. In the Details pane of the BP_RTreeCharacter
editor, search for "manny" for the Anim Class
within the Animation
section. Select ABP_Manny
. Note - this will show up as ABP_Manny_C
after you select it.
Next, we need to select a skeletal mesh. On the Skeletal Mesh
dropdown menu, select SKM_Manny_Simple
. See screenshot:
At this point, a skeletal mesh should have appeared in the viewport. We want to move and rotate it so it is facing towards the arrow.
I am not an animation expert, and there is one last thing we need to do to get our character to run around the level. On the details pane under the animation section, select "Browse to asset in content browser" for the Anim Class (should be ABP_Manny_C
). Open the animation blueprint in the content browser. In the event graph, we need to find the "Event BLueprint Update Animation" part. Look at the diagram below and remove one connection while adding a new one. This is just a quick hack to get the animation to work and I recommend learning more about the animation system (I need to as well!).
Next, we need to set the BP_RTreeTutorialAIController
which we created earlier as the AI Controller Class. On the Components tab on the left side of the editor, select BP_RTreeCharacter (Self)
, and then on the right side of the window in the details tab search "control".
In the AI Controller Class
droptown, select BP_RTreeTutorialAIController
.
Finally, we need to add our RTreeGoToGameplayAbility to our character as a startup ability. in the details pain on the BP_RTreeCharacter editor, search "abilities". For startup abilities, make sure to add, "RTreeGoToGameplayAbility".
Click save followed by Compile in the top-left of the Unreal Editor.
Build and configure user input widget
With our character (mostly) configured, we need to create a text box which will allow the player to give commands to the RTree-backed NPC. To do this, we will use a User Widgets blueprint. You can follow along below, but if you prefer video, this video is roughly the workflow we follow.
In the content browser, right click within the Content/
directory and search widget. Select "Widget Blueprint"
Rename the widget blueprint "WB_RTreeInputText", save and move it to the Content/Blueprints/
directory. Open the blueprint.
Once the blueprint editor has opened, search for "canvas" in the Palette on the left side of the editor and drag it down into the hierarchy.
Next, we need to add a text box to our canvas.
After adding the text box, drag it down towards the bottom-right of the canvas and resize it to make it wider and slightly taller. This is going to be our text input so we want space for basic commands.
Now, switch back to our BP_RTreeCharacter
editor. If you don't see the viewport tab or event graph tab, make sure to open the full blueprint editor.
Once in the editor, open the eventgraph and left click drag off the event begin play node. When you release the click, a dropdown will appear. Search for "construct" and select the "Construct Object from Class" option.
On the new node, click on "Class" and search "WB". We should see our WB_RTreeInputText widget blueprint show. Select it.
Left-click drag off of the execution flow pin on the Construct Object widget and release. Search for "Add to viewport" and select it. Also connect the Construct Object's "Return Value" pin to the Add to Viewport's "Target" pin. You should now have:
From here, left-click drag off of the Add to Viewport node and release, searching for and selecting "Set Input Mode Game and UI". In the open space to the left of the newly placed node, right click and search for/select "Get Player Controller". Connect Get Player Controller's Return Value to the Player Controller pin on Set Input Mode Game and UI. Finally, left-click drag once again from the Get Player Controller Return Value and release, searching for and selecting "Show Mouse Cursor".
Select the "Show Mouse Cursor" checkbox and connect the new SET node execution pin to the exiting execution pin of the Set Input Mode Game And UI node. The final configuration should look like:
s
Now, open up WB_RTreeInputText
and select the "Graph" button on the top right of the editor.
On the "My Blueprint" tab on the left, under "VARIABLES", select "EditableTextBox_1". In the details pane below, click on the Events sub-option, "On Text Committed".
Left-click drag from the execution pin from "On Text Committed (EditableTextBox_1)" and search/select "branch".
Next, click-drag off of "Commit Method" and search "equal". Select "Equal (Enum)". On the drop down on the new node, select, "On Enter". Drag from the red pin on the right to the "Condition" pin on the Branch node.
Click-drag off of "True" on the Branch node. Search/Select, "Get All Actors of Class". For the Actor Class, search "BP_RTreeCharacter".
Drag off of the right execution pin and search/select "For Each Loop". Connect the "Out Actors" pin of the Get All Actors of Class node to the "Array" pin on the For Each Loop node.
Next, drag off of the "Loop Body" execution pin and search select "Cast to RTreeAIController". Note, we want the top option, not the AI Controller Class version (we need an instance of the class).
Left-click drag from the "Array Element" pin on the "For Each Loop" and search/select "Get Controller". Connect this new node's "Return Value" pin to the "Object" pin on the Cast to RTreeAIController node.
Click drag off of the "Cast Failed" pain and search/select "Print String". We should print a message if the cast fails. Change the In String to "Cast Failed!".
Click drag off of the execution pinr on Cast To RTreeAIController and search/select, "Set Player Prompt". Connect the "As RTree AIController" pin on the previous node to Set Player Prompt's "Target" pin.
Finally connect the "Text" pin on the very first node we placed (On Text Committed (EditableTextBox_1) ) to the "In Player Prompt" pin on the Set Player Prompt node.
Right click just to the right of the Set Player Prompt Node and search/select "get editabletextbox_1".
Click-drag off the pin and search "content set text". Connect The execution pin from the Set Player Prompt Node to the new Set node.
In total, the graph should look like:
Save and compile.
Add RTree NPC to game
We're almost done! Now we just need to add our NPC and we should be good to go.
Back in the main editor, open the content drawer and in Content/Blueprints/
click and drag "BP_RTreeCharacter" into the level. You may want to configure their stance.
Add Navmesh
For the final step, we want to cover the entire level in a navmesh so our movement commands work.
Expand and manuver the volume so the entire level is within it.
Save and compile.
Playing the game
Congratulations!! You've completed the tutorial and should now be able to launch the editor and play around with our NPC character. You can give commands to the character via the text box or just walk around the level yourself. Movement is ASWD, and to control where you look, hold down the right mouse in the viewport to look around.
Some interesting things to try:
"Go to 3 signs which sum up to 9"
"Go to all signs"
"Go to all signs in descending order"
Have fun!