Posted on August 2026
In this tutorial, we're going to continue working on the Utility AI system. In this second tutorial, we will setup the Npc Brain Component class to handle the logic the NPC will perform.
Go to Tools -> New C++ Class -> All Classes.
Select Actor Component and name it NpcBrainComponent. Click Create Class.
Now inside the NpcBrainComponent.h file, paste the following:
Note: If you named it something custom, replace the class name in the code below. You will also have to replace YOUTUBETUTORIALS_V3_API with your project name
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "NpcTaskDefinition.h"
#include "Components/ActorComponent.h"
#include "NpcBrainComponent.generated.h"
class UNpcTaskDefinition;
USTRUCT(BlueprintType)
struct FNpcDynamicTask
{
GENERATED_BODY()
// A task thats being added dynamically
UPROPERTY(EditAnywhere, Instanced)
UNpcTaskDefinition* Task = nullptr;
// Should we remove this task when its finished?
UPROPERTY(EditAnywhere)
bool bRemoveWhenFinished = true;
// Should this task be enabled
UPROPERTY(EditAnywhere)
bool bEnabled = true;
};
UENUM()
enum class ENpcTaskSource: uint8
{
AlwaysAvailableTasks,
DynamicTasks
};
USTRUCT()
struct FNpcTaskInstance
{
GENERATED_BODY()
UPROPERTY()
UNpcTaskDefinition* DefinitionTask = nullptr;
UPROPERTY()
UNpcTaskDefinition* RuntimeTask = nullptr;
UPROPERTY()
ENpcTaskSource Source = ENpcTaskSource::AlwaysAvailableTasks;
int32 DynamicIndex = INDEX_NONE;
};
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class YOUTUBETUTORIALS_V3_API UNpcBrainComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UNpcBrainComponent();
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
// Tasks that the NPC is always allowed to do
UPROPERTY(EditAnywhere, Instanced, Category="NpcBrainComponent|Tasks")
TArray AlwaysAvailableTasks;
// Tasks that are dynamically added from quests, events, etc...
UPROPERTY(EditAnywhere, Category="NpcBrainComponent|Tasks")
TArray DynamicTasks;
// Function to handle evaluating tasks
void EvaluateTasks();
// Build the brain context for each task
FNpcBrainContext BuildBrainContext();
// When the current active task has finished
UFUNCTION()
void OnActiveTaskFinished(UNpcTaskDefinition* Task, FNpcBrainContext Context);
protected:
// Called when the game starts
virtual void BeginPlay() override;
private:
// The active task in the current execution
UPROPERTY(EditAnywhere, Category="NpcBrainComponent|Tasks|Cache")
FNpcTaskInstance ActiveTask;
// The pending task in the next execution
UPROPERTY(EditAnywhere, Category="NpcBrainComponent|Tasks|Cache")
FNpcTaskInstance PendingTask;
// How often will the brain component re-evaluate
UPROPERTY(EditAnywhere, Category="NpcBrainComponent|Timers")
float EvaluationInterval = 5.0f;
// How much higher does a score have to be to switch to a new task
UPROPERTY(EditAnywhere, Category="NpcBrainComponent|Timers")
float SwitchInterval = 0.15f;
// When the last evaluation was performed to prevent spam
float TimeSinceLastEvaluation = 0.f;
// The timer handle for the evaluation timer
FTimerHandle EvaluationTimerHandle;
// Start the task helper function
UFUNCTION()
void StartTask(FNpcTaskInstance& Task, FNpcBrainContext& Context);
// Stop the task helper function
UFUNCTION()
void StopTask();
};
Now inside NpcBrainComponent.cpp paste the following:
// Fill out your copyright notice in the Description page of Project Settings.
#include "NpcBrainComponent.h"
#include "AbilitySystemComponent.h"
// Sets default values for this component's properties
UNpcBrainComponent::UNpcBrainComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = true;
// ...
}
// Called when the game starts
void UNpcBrainComponent::BeginPlay()
{
Super::BeginPlay();
GetWorld()->GetTimerManager().SetTimer(
EvaluationTimerHandle,
this,
&UNpcBrainComponent::EvaluateTasks,
EvaluationInterval,
true
);
// Call straight away
EvaluateTasks();
}
void UNpcBrainComponent::StartTask(FNpcTaskInstance& Task, FNpcBrainContext& Context)
{
ActiveTask = Task;
ActiveTask.RuntimeTask = DuplicateObject(Task.DefinitionTask, this);
ActiveTask.RuntimeTask->OnTaskFinished.AddDynamic(this, &UNpcBrainComponent::OnActiveTaskFinished);
ActiveTask.RuntimeTask->Execute(Context);
}
void UNpcBrainComponent::StopTask()
{
if (ActiveTask.RuntimeTask && ActiveTask.RuntimeTask->IsValidLowLevel())
{
ActiveTask.RuntimeTask->OnTaskFinished.RemoveDynamic(this, &UNpcBrainComponent::OnActiveTaskFinished);
ActiveTask.RuntimeTask->FinishTask(BuildBrainContext());
ActiveTask.RuntimeTask = nullptr;
}
}
// Called every frame
void UNpcBrainComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (ActiveTask.RuntimeTask)
{
ActiveTask.RuntimeTask->TickTask(BuildBrainContext());
}
}
void UNpcBrainComponent::EvaluateTasks()
{
if (!GetWorld())
return;
TArray Candidates;
// Always available tasks
for (UNpcTaskDefinition* Task : AlwaysAvailableTasks)
{
FNpcTaskInstance Candidate;
Candidate.DefinitionTask = Task;
Candidate.Source = ENpcTaskSource::AlwaysAvailableTasks;
Candidates.Add(Candidate);
}
// Dynamic Tasks
for (int32 i = 0; i < DynamicTasks.Num(); i++)
{
FNpcTaskInstance Candidate;
Candidate.DefinitionTask = DynamicTasks[i].Task;
Candidate.Source = ENpcTaskSource::DynamicTasks;
Candidate.DynamicIndex = i;
Candidates.Add(Candidate);
}
// Store the current brain context
FNpcBrainContext context = BuildBrainContext();
FNpcTaskInstance BestTask;
float BestScore = -1.f;
for (FNpcTaskInstance& Candidate : Candidates)
{
if (!Candidate.DefinitionTask)
continue;
if (!Candidate.DefinitionTask->CanRun(context))
continue;
if (Candidate.DefinitionTask->bForceSelection)
{
BestTask = Candidate;
break;
}
float Score = Candidate.DefinitionTask->Score(context)
+ Candidate.DefinitionTask->BaseScore
+ Candidate.DefinitionTask->Priority * 0.01f;
if (Score > BestScore)
{
BestScore = Score;
BestTask = Candidate;
}
}
// If no best task, return
if (!BestTask.DefinitionTask)
return;
// If no active task, start the best task
if (!ActiveTask.DefinitionTask)
{
StartTask(BestTask, context);
return;
}
// Check if the active task can be interrupted
if (!ActiveTask.DefinitionTask->bCanBeInterrupted)
{
PendingTask = BestTask;
return;
}
// Compare scores if its over our threshold base score
float CurrentScore = ActiveTask.DefinitionTask->Score(context)
+ ActiveTask.DefinitionTask->BaseScore
+ ActiveTask.DefinitionTask->Priority * 0.01f;
if (BestScore > CurrentScore + SwitchInterval)
{
StopTask();
StartTask(BestTask, context);
}
}
FNpcBrainContext UNpcBrainComponent::BuildBrainContext()
{
FNpcBrainContext Context;
Context.Pawn = Cast(GetOwner());
if (Context.Pawn)
{
Context.ASC = Context.Pawn->FindComponentByClass();
}
Context.WorldTime = GetWorld() ? GetWorld()->GetTimeSeconds() : 0.0f;
return Context;
}
void UNpcBrainComponent::OnActiveTaskFinished(UNpcTaskDefinition* Task, FNpcBrainContext Context)
{
if (ActiveTask.DynamicIndex != INDEX_NONE)
{
if (DynamicTasks[ActiveTask.DynamicIndex].bRemoveWhenFinished)
{
DynamicTasks.RemoveAtSwap(ActiveTask.DynamicIndex);
}
}
ActiveTask = FNpcTaskInstance(); // reset the active task
if (PendingTask.DefinitionTask)
{
StartTask(PendingTask, Context);
PendingTask = FNpcTaskInstance();
return;
}
EvaluateTasks();
}
The C++ is now all done. Compile the code and open Unreal. Open your NPC and add the NPCBrainComponent to the NPC's blueprint.
Now inside the components details, you can set the Always Available Tasks or the Dynamic Tasks for your NPC.