Utility AI Plugin

Overview

The Utility AI plugin aims to expand upon Dave Mark’s Infinite Axis Utility System (IAUS) and enables fine-tuning of a utility agent’s behavior in response to numerous factors. Specifically, the plugin provides the user the ability to:

  • compose utility considerations and decisions using C++ and Blueprints
  • assign weights to decisions to prioritize some over other decisions
  • generate custom, displayable input parameters
  • create response curves using preset curve functions or runtime float curve
  • re-evaluate a decision with different permutations of context on-the-fly

For an example usage, check out Project Bongo.

Components

The main components to consider in this plugin:

  • UtilityAIComponent - the actor component to attach to an agent actor; contains a single DecisionMaker object
  • DecisionMaker - an object that stores a list of Decisions the agent can make
  • Decision - an object that contains a list of Considerations to evaluate
  • Consideration - an input representing a calculated probability score
Available nodes for the Utility AI plugin

Creating Considerations and Decisions

The following videos showcase the setup of the building blocks that allow an AI to make utility-driven decisions.

Making Decisions

While the decision-scoring function could simply just return the value from FUtilityDecisionScoreEvaluator::Score(), I wanted to add an optional, built-in implementation for making a single decision based on a number of different permutations of the decision’s context.

For example - in a turn-based combat where each team has more than one party member - if an enemy AI wants decide which opponent to target with an ability, it needs to evaluate all (or some, if pruning) Considerations in the context of each set of possible targets.

Method called for making decision

bool UUtilityDecisionMaker::MakeDecision(const FUtilityDecisionContextHandle& InContextHandle, UUtilityDecision*& OutDecision)
{
	check(InContextHandle.GetDecidingAgent());

	if (!ActorInfo->OwnerUtilityAIComponent->CanMakeDecision())
	{
		UTILITY_AI_LOG(Warning, TEXT("Attempting to call MakeDecision, but deciding agent %s cannot make decisions."), *InContextHandle.Get()->GetDecidingAgent()->GetName());
		return false;
	}

	if (Decisions.IsEmpty()) return false;

	ScoreAllDecisions(InContextHandle);

	// Copy decisions list since we want to remove decisions that have scored 0
	TArray<TObjectPtr<UUtilityDecision>> FilteredDecisions = Decisions;
	FilteredDecisions.RemoveAll([](const TObjectPtr<UUtilityDecision> Decision)
	{
		return FMath::IsNearlyZero(Decision->Score);
	});

	OutDecision = ChooseDecisionByMethod(FilteredDecisions, DecisionMethod, DecisionPercentileValue);
	return OutDecision != nullptr;
}

Method for scoring all decisions

void UUtilityDecisionMaker::ScoreAllDecisions(const FUtilityDecisionContextHandle& InContextHandle)
{
	for (const auto& Decision : Decisions)
	{
		if (Decision)
		{
			Decision->SetContextHandle(InContextHandle);

			// In the while-statement, bShouldReevaluateDecision is true if the function is found in the UtilityAIComponent (attached to agent).
			// The OnReevaluateDecision call can be overridden to decide on what condition it should re-evaluate.
			// In OnReevaluateDecision, the user can make changes to the existing context to handle cases where permutations of actions are needed.
			do
			{
				// Last chance to modify the context handle
				if (ActorInfo->OwnerUtilityAIComponent->bShouldModifyDecisionContext)
				{
					OnModifyDecisionContext(InContextHandle, Decision.Get());
				}

				Decision->Score = FUtilityDecisionScoreEvaluator::Score(Decision.Get()) * Decision->Weight;

				// Only add non-zero scores to evaluations
				if (Decision->Score > 0.f)
				{
					// Adding decision score evaluation - we create a copy of the decision context to potentially assign later
					FUtilityDecisionContextHandle EvalContextHandle = FUtilityDecisionContextHandle(Decision->GetContextHandle()->Get());
					DecisionEvaluations.FindOrAdd(Decision).Add(FUtilityDecisionEvaluation(Decision->Score, EvalContextHandle));
				}
			}
			while (ActorInfo->OwnerUtilityAIComponent->bShouldReevaluateDecision && ActorInfo->OwnerUtilityAIComponent->OnReevaluateDecision(InContextHandle, Decision.Get()));

			// Evaluate final decision score
			if (const auto Evaluations = DecisionEvaluations.Find(Decision.Get()))
			{
				if (const auto Evaluation = ChooseDecisionEvaluationByMethod(*Evaluations, DecisionMethod, DecisionPercentileValue))
				{
					// Assign winning evaluation properties to decision
					Decision->Score = Evaluation->Score;
					Decision->SetContextHandle(Evaluation->ContextHandle);
				}

				Evaluations->Empty();
			}
		}
	}
}

Visual Logging

I used Unreal Engine’s Visual Logger to view the decisions being made by an agent at a selected frame.

We see that "UDM_Creature_Frost" decided to execute the 3 basic attacks consecutively.
Tony Nguyen
Tony Nguyen
Technical Game Designer

Tenacious game developer with an unwavering passion for overcoming game development challenges.