Home Games Resources Assets Contact

Check us out
on social media

Utility AI In UE5 : Part 2

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.

STEP 1 - Create a C++ class

Go to Tools -> New C++ Class -> All Classes. Select Actor Component and name it NpcBrainComponent. Click Create Class.

Create a new C++ class (NpcBrainComponent)

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();
}

STEP 3 - ADD COMPONENT TO NPC

The C++ is now all done. Compile the code and open Unreal. Open your NPC and add the NPCBrainComponent to the NPC's blueprint.

Adding component to NPC

Now inside the components details, you can set the Always Available Tasks or the Dynamic Tasks for your NPC.

Adding tasks to Brain component
× Surprise GIF