HomeGamesResourcesAssetsContact

Check us out
on social media

Utility AI In UE5 : Part 4

Posted on September 2026

In this tutorial, we're going to continue working on the Utility AI system. In this third tutorial, we will add in the AI schedules.

STEP 1 - Modify the existing NpcSchedulePeriod

Inside the NpcSchedulePeriod.h modify it to add the new enum:

class UNpcTaskDefinition;

// Enum for types of schedule to run
UENUM()
enum class ENpcScheduleMode: uint8 {
	HighestScore,
	Random,
	Sequence
};

Also modify the struct to have the two new variables:

// Enum to decide what schedule mode the periods should run in
	UPROPERTY(EditAnywhere)
	ENpcScheduleMode ScheduleMode = ENpcScheduleMode::HighestScore;
	
	// Get all the tasks that can be run
	TArray<FNpcTaskInstance> GetCandiates(UNpcBrainComponent* BrainComponent, int32 PeriodIndex);

Inside the NpcSchedulePeriod.cpp add:

TArray<FNpcTaskInstance> FNpcSchedulePeriod::GetCandiates(UNpcBrainComponent* BrainComponent, int32 PeriodIndex)
{
	TArray<FNpcTaskInstance> Candidates;
	
	if (!BrainComponent)
	{
		return Candidates;
	}
	
	switch (ScheduleMode)
	{
		case ENpcScheduleMode::HighestScore:
		{
			for (UNpcTaskDefinition* TaskDefinition : Tasks)
			{
				if (!TaskDefinition)
				{
					continue;
				}
				
				FNpcTaskInstance Candidate;
				Candidate.DefinitionTask = TaskDefinition;
				Candidate.Source = ENpcTaskSource::Schedule;
				Candidate.SchedulePeriodIndex = PeriodIndex;
				Candidates.Add(Candidate);
			}
			
			break;
		}
	
	case ENpcScheduleMode::Random:
		{
			if (Tasks.Num() == 0)
			{
				break;
			}
			
			const int32 RandomIndex = FMath::RandRange(0, Tasks.Num() - 1);
			
			if (Tasks[RandomIndex])
			{
				FNpcTaskInstance Candidate;
				Candidate.DefinitionTask = Tasks[RandomIndex];
				Candidate.Source = ENpcTaskSource::Schedule;
				Candidate.SchedulePeriodIndex = PeriodIndex;
				Candidates.Add(Candidate);
			}
			
			break;
			
		}
	
	case ENpcScheduleMode::Sequence:
		{
			if (Tasks.Num() == 0)
			{
				break;
			}
			
			FNpcScheduleSequenceState& SequenceState = BrainComponent->GetScheduleSequenceState(PeriodIndex);
			
			const int32 NextIndex = (SequenceState.LastTaskIndex + 1) % Tasks.Num();
			
			if (Tasks[NextIndex])
			{
				FNpcTaskInstance Candidate;
				Candidate.DefinitionTask = Tasks[NextIndex];
				Candidate.Source = ENpcTaskSource::Schedule;
				Candidate.SchedulePeriodIndex = PeriodIndex;
				Candidates.Add(Candidate);
			}
			
			break;
			
		}
	}

	return Candidates;
}

STEP 2 - Modify the existing NpcScheduleDataAsset

Inside the NpcScheduleDataAsset.h modify it to add the new varaibles:

// Get all the tasks for the current time
	TArray<FNpcTaskInstance> GetTasksForTime(float CurrentTime, UNpcBrainComponent* BrainComponent);
	
	// Get the current active period indices
	TArray<int32> GetActivePeriodIndices(float CurrentTime);

Inside the NpcScheduleDataAsset.cpp add:

TArray<FNpcTaskInstance> UNpcScheduleDataAsset::GetTasksForTime(float CurrentTime, UNpcBrainComponent* BrainComponent)
{
	TArray<FNpcTaskInstance> Tasks;
	
	for (int32 PeriodIndex : GetActivePeriodIndices(CurrentTime))
	{
		FNpcSchedulePeriod& Period = Periods[PeriodIndex];
		
		Tasks.Append(Period.GetCandiates(BrainComponent, PeriodIndex));
	}
	
	if (Tasks.Num() > 0)
		return Tasks;

	return FallbackTasks;
}

TArray<int32> UNpcScheduleDataAsset::GetActivePeriodIndices(float CurrentTime)
{
	TArray<int32> ActivePeriods;
	
	for (int32 i = 0; i < Periods.Num(); i++)
	{
		if (Periods[i].IsTimeInRange(CurrentTime))
		{
			ActivePeriods.Add(i);
		}
	}

	return ActivePeriods;
}

STEP 3 - Modify the existing NpcBrainComponent

Inside the NpcBrainComponent.h modify the FNpcTaskInstance struct to be:

USTRUCT()
struct FNpcTaskInstance
{
	GENERATED_BODY()
	
	UPROPERTY()
	UNpcTaskDefinition* DefinitionTask = nullptr;
	
	UPROPERTY()
	UNpcTaskDefinition* RuntimeTask = nullptr;
	
	UPROPERTY()
	ENpcTaskSource Source = ENpcTaskSource::AlwaysAvailableTasks;
	
	int32 DynamicIndex = INDEX_NONE;
	
	UPROPERTY()
	int32 SchedulePeriodIndex = INDEX_NONE;
};

Inside the NpcBrainComponent.h add a new struct for storing the schedules state:

USTRUCT(BlueprintType)
struct FNpcScheduleSequenceState
{
	GENERATED_BODY()
	
	UPROPERTY()
	int32 LastTaskIndex = INDEX_NONE;
	
	UPROPERTY()
	bool bWasInPeriod = false;
};

Inside the NpcBrainComponent.h add a new variable for storing the schedules state:

// Store the schedules sequence states
	UPROPERTY()
	TMap<int32, FNpcScheduleSequenceState> ScheduleSequenceStates;

Inside the NpcBrainComponent.h add two new functions for getting and updating the state:

// Get the schedule sequence state 
	FNpcScheduleSequenceState& GetScheduleSequenceState(int32 PeriodIndex);
	
	// Update the schedule period states
	void UpdateSchedulePeriodStates(const TArray<int32>& ActivePeriodIndices);

Inside the NpcBrainComponent.cpp add the two new functions:

FNpcScheduleSequenceState& UNpcBrainComponent::GetScheduleSequenceState(int32 PeriodIndex)
{
	return ScheduleSequenceStates.FindOrAdd(PeriodIndex);
}

void UNpcBrainComponent::UpdateSchedulePeriodStates(const TArray<int32>& ActivePeriodIndices)
{
	TSet<int32> ActiveSets;
	
	for (int32 PeriodIndex : ActivePeriodIndices)
	{
		ActiveSets.Add(PeriodIndex);
		
		FNpcScheduleSequenceState& State = GetScheduleSequenceState(PeriodIndex);
		
		State.bWasInPeriod = true;
	}
	
	for (auto& Pair : ScheduleSequenceStates)
	{
		const int32 PeriodIndex = Pair.Key;
		FNpcScheduleSequenceState& State = Pair.Value;
		
		if (!ActiveSets.Contains(PeriodIndex) && State.bWasInPeriod)
		{
			State.bWasInPeriod = false;
			State.LastTaskIndex = INDEX_NONE;
		}
	}
}

Inside the NpcBrainComponent.cpp modify the EvaluateTasks function to be:

void UNpcBrainComponent::EvaluateTasks()
{
	if (!GetWorld())
		return;
	
	TArray<FNpcTaskInstance> Candidates;
	
	// Get the current scheduled tasks
	const float CurrentTime = GetCurrentTime();
	
	if (Schedule)
	{
		const TArray<int32> ActivePeriodIndices = Schedule->GetActivePeriodIndices(CurrentTime);
		
		UpdateSchedulePeriodStates(ActivePeriodIndices);
		
		Candidates.Append(Schedule->GetTasksForTime(CurrentTime, this));
	}
	
	// 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);
	}
}

Inside the NpcBrainComponent.cpp modify the StartTask function to set the sequence if needed:

void UNpcBrainComponent::StartTask(FNpcTaskInstance& Task, FNpcBrainContext& Context)
{
	if (Task.Source == ENpcTaskSource::Schedule && Task.SchedulePeriodIndex != INDEX_NONE && Schedule)
	{
		if (Schedule->Periods.IsValidIndex(Task.SchedulePeriodIndex))
		{
			const FNpcSchedulePeriod& Period = Schedule->Periods[Task.SchedulePeriodIndex];

			if (Period.ScheduleMode == ENpcScheduleMode::Sequence)
			{
				FNpcScheduleSequenceState& State = GetScheduleSequenceState(Task.SchedulePeriodIndex);
				
				State.LastTaskIndex = Period.Tasks.IndexOfByKey(Task.DefinitionTask);
			}
		}
	}
	
	ActiveTask = Task;
	
	ActiveTask.RuntimeTask = DuplicateObject(Task.DefinitionTask, this);
	
	ActiveTask.RuntimeTask->OnTaskFinished.AddDynamic(this, &UNpcBrainComponent::OnActiveTaskFinished);
	
	ActiveTask.RuntimeTask->Execute(Context);
}