Skip to content

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.

Project Creation

Once the project completes building, you should see something similar to the following:

Starting Editor Window

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).

  1. On the Edit menu on the top left of the Unreal Editor, select "Editor Preferences".

  2. 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.

    Disable Live Coding

  3. 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 under Hot Reload is unchecked.

    Disable Automatic Compilation of New Classes

  4. 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.

    Rider Integration Plugin Selection

  5. 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 the Source Code Editor attribute is set to "Rider Uproject".

    Rider Source Code Editor

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:

Enable RTree Plugin

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:

Optionally update model file path

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.

Source/RTreeTutorial/RTreeTutorial.Build.cs
// 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)!
  });
 }
}
  1. These are default dependencies.
  2. 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:

  1. Subclass ARTreeAIController (instead of UE5's standard, AAIController)
  2. Implent virtual FString ProcessObservations(TArray<RTreeObservation>& Observations) override on the subclass.
  3. 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.

RTreeAIController

Select RTreeAIController and click next. This will open the following menu:

RTreeTutorialAIController

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:

  1. 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 to OnTargetPerceptionUpdated, 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.
  2. 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:

  1. Give developers control over how perceptions are rendered as text.
  2. 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;
}
  1. Be sure to include this if you want to modify the default sight configuration below (needs UAIPerceptionComponent).
  2. Below we configure the SightConfig. The values below are the defaults set in the parent RTreeAIController, however you may override any of those here.
  3. 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.
  4. 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).
  5. 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:

  1. 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.
  2. Validating if an action can be activated according to in-game criteria.
  3. 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.
  4. 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.
  5. 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?
  1. 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
  2. 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:

  1. Plan (ordered list of JSON function calls) generated by model.
  2. 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.
  3. 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.
  4. Once all of the steps have been enqueued, we tell the owning controller to start the plan.
  5. The owning controller dequeues the struct at the front of the plan queue
  6. 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's ActivateAbility() 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.

AbilitySystemComponentSearch

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:

RTreeAbilitySystemComponentName

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:

public:
    void AddCharacterAbilities(const TArray<TSubclassOf<UGameplayAbility>>& StartupAbilities);

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)!
    }
}
  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.

TutorialLoggingMacroHeader

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:

#include "RTreeTutLogging.h"

DEFINE_LOG_CATEGORY(LogRTreeTut)

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/.

NameAbilityTask

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)! 
};
  1. 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.
  2. Note the static constructor which is what we will call from the ability when instantiating/configuring the ability task.
  3. In the implementation, we will call .Broadcast() on these delegates depending on certain criteria.
  4. In the implementation, we will call .Broadcast() on these delegates depending on certain criteria.
  5. 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/.

NameGoToGameplayAbility

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();
};
  1. Make sure to add this!
  2. We also have to inherit from IRTreeAbilityInterface so RTree knows to use this ability.
  3. This is where we will define what the ability does.
  4. 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".

RTreeCharacterSetup1

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.

  1. We need to add the import, #include "AbilitySystemInterface.h" for the GAS. Also, make sure to add a second inheritance class, public IAbilitySystemInterface.
  2. We can remove the entire second public block, as we won't be using Tick() or SetupPlayerInputComponent().
  3. In the protected section add:

    1. virtual void PossessedBy(AController* NewController) override; Per Epic's C++ style guide, when overriding a method, we use both virtual and override keywords.
    2.     UPROPERTY()
          TObjectPtr<UAbilitySystemComponent> AbilitySystemComponent;
      
    3. void AddCharacterAbilities();
  4. Add a private section at the bottom of the class and add:

        UPROPERTY(EditAnywhere, Category="Abilities")
        TArray<TSubclassOf<UGameplayAbility>> StartupAbilities;
    

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:

CompileAndOpen

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:

ReturningToTheEditor

Right click on the mouse and, using the wasd keys, move the camera up so you can see the entire scene like the below:

StartToLevelScene

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:

RemovedCubes

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:

AddCube

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.

ReshapeCube

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.

AddTextRenderer

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.

RepositionText

ConfigureText

Finally, we should have the text facing the correct direction and centered on our sign. Change the text to "1".

CompletedSign

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.

AddRTreeDetectable

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.

ConfigureRTreeDetectable

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:

AIPerceptionStimuliSourceConfig

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.

CopySign4x

Next, let's update the content on the sign and in its RTreeDetectable component. Note, we are still not updating the movement location attribute.

UpdateSignsContent

After updating the sign text and its RTreeDetectable compliment, let's orient the signs in a way so they face towards the corner.

OrientSigns

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.

CopyLocation

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

CreateBlueprintForAIController

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.

ConfigurePerception

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.

ConfigureSightRadius

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:

CreateBPClass

Let's name our new class BP_RTreeCharacter and put it in Content/Blueprints/.

NameBPClass

After creating the class, you should see the following blueprint editor:

BPEditor

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.

AddContentPack

Select the ThirdPerson content on the Blueprint tab.

AddThirdPerson

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.

AnimClass

Next, we need to select a skeletal mesh. On the Skeletal Mesh dropdown menu, select SKM_Manny_Simple. See screenshot:

SkeletalMesh

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.

RepositionedMesh

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!).

AnimationHack

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.

SelectAIController

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".

AddGoToGameplayAbility

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"

SearchWidgetBlueprint

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.

DragCanvasDown

Next, we need to add a text box to our canvas.

AddTextBox

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.

MoveAndResizeTextBox

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.

ConstructWidgetObject

On the new node, click on "Class" and search "WB". We should see our WB_RTreeInputText widget blueprint show. Select it.

SelectObjectClass

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:

AddToViewport

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".

SetInputModeGetPlayerControllerShowMouseCursor

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:

BP_RTreeCharacterEventGraphFinals

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".

OnTextCommittedEvent

Left-click drag from the execution pin from "On Text Committed (EditableTextBox_1)" and search/select "branch".

AddBranch

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.

OnEqual

Click-drag off of "True" on the Branch node. Search/Select, "Get All Actors of Class". For the Actor Class, search "BP_RTreeCharacter".

GetAllActorsOfClass

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.

ForEach

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).

CastToRTree

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.

GetController

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:

CompleteGraph

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.

AddNavMesh

Expand and manuver the volume so the entire level is within it.

NavMeshEncapsulate

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"

FinalDemo

Have fun!