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 singleDecisionMaker
objectDecisionMaker
- an object that stores a list ofDecisions
the agent can makeDecision
- an object that contains a list ofConsiderations
to evaluateConsideration
- 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.