Skip to content
mastercoms edited this page Aug 23, 2021 · 6 revisions

This document will cover the overall design of the AI system used in Project Borealis. The first section briefly explains the necessary theory behind the AI system and provides different in-depth learning resources. The second section presents an overview on the AI system and how to tune its parameters. Finally, the last section explains how to implement new "considerations", which determine when behaviors should be executed by the AI system.

IAUS Overview

Project Borealis' AI system is a custom implementation of the Infinite Axis Utility System (IAUS). IAUS is based on Utility Theory, where the basic premise is that AI characters score their possible actions, and pick from the top scoring options. IAUS has three components: considerations, contexts, and behaviors.

A consideration takes a single input, like self health, and outputs a score using a response curve. A response curve is a mathematical function, and is the basis of tuning AI in this set up. More detail on response curves will be given in the following section.

A context is the target of a behavior. It can be an enemy, neutral, ally, or the AI character itself.

A behavior is the routine that the AI character will perform. Each behavior has a list of considerations which are evaluated per context and multiplied together to get the final score. The behavior and context combination with the highest final score will be selected. Additionally, the behavior that the AI character is currently performing receives an inertia bonus, increasing the score by a constant factor, which prevents flip flopping between behaviors.

For more information about IAUS and Utility Theory, we recommend the following learning resources:

IAUS in UE4 Behavior Trees

Project Borealis has a custom implementation of IAUS within UE4's Behavior Tree system. This allows us to easily use the full feature-set of Unreal's AI system, while having the flexibility and expressiveness of a Utility AI system.

As an example, we will look at the Combine Overwatch soldier Behavior Tree, as it covers most of the features of the AI system:

Note that below the Behavior Tree's root there is a Utility node with multiple children representing the different behaviors of the AI character (Retreat, Move Closer, Throw Grenade, etc.). Behaviors are composed of decorators representing considerations, and the Utility node is charge of evaluating them to determine which behavior should be executed.

The Target Selector decorator on the Utility node allows the designer to adjust some parameters of the Utility system:

Inertia Weight: Currently, this value does not represent actual "inertia" in Utility Theory, but a time duration that limits how often a new behavior should be chosen. In other words, this acts like a cooldown to switch between behaviors. In the future, this will be changed to represent the time duration for the inertia bonus.

Blackboard Key: The blackboard key where currently selected context is stored.

Typically, you will not need to change these from their initial set up.

Now, let's look at the Retreat behavior:

This is composed of the considerations (decorators) Target Prioritization, Health, and Distance, and a service node that finds a position to cover from attacks. The behavior settings can be configured in the details panel of the "Retreat" node.

The Target flags tell which contexts to evaluate this behavior with:

  • Target Self: the AI character itself.
  • Target Friendly: characters within the same team or with a friendly relationship to it.
  • Target Neutral: characters with no relationship to the AI character's own team.
  • Target Hostile: characters with an enemy relationship to the AI character's own team.

This means that a behavior may have multiple characters to be evaluated with. Therefore, IAUS will select the behavior and context combination with the highest final score.

Initial Weight: Determines the starting score to multiply consideration scores by. Essentially, it is a limit to how high this score can go, as 0.95 * 1 * 1 * 1 = 0.95, so even if all considerations have a high score, it will be limited to 0.95.

Node Name: The name of the behavior.

Now, let's look at the Health consideration on the Retreat behavior:

In the consideration's details panel, you can see a preview of the current response curve and adjust it:

  • The X axis of this curve is the input from Minimum to Maximum. Typically, this range is determined automatically by the consideration type, but can be tuned if necessary. Any input outside the range will be clamped to within bounds.
  • The Y axis is the output score.

There are several response curves to choose from, each with its own settings:

You can drag the parameters around to tune your response curve easily, and see the effect of each adjustment in the preview image.

In this example, Target Self is checked, which is a specific property of the Health consideration. It means that the AI character checks its own health, rather than the context's health. We are scoring high for lower amounts of health, and start to drop off at around 30% health. This means that when the AI character has lower health, the Retreat behavior will be scored higher, and thus may be selected more often.

For more information about response curve design, we recommend the following learning resources:

Finally, you can add new considerations by right clicking the behavior and selecting "Add Decorator". Note that putting decorators that are not considerations does not make sense in the context of IAUS, and may crash the Engine at runtime.

Implementing Considerations

Note: These considerations are not available in the base plugin at this time, but can serve as a basis for your own game.

Implementing new considerations in C++ is the basis of creating new behaviors for our AI characters in Project Borealis. To explain how to do so, we will dissect and analyze the consideration IsMovementObstructed, which is used in the Zombie's behavior Break Obstacle.

This consideration is defined by the class UIAUSConsideration_IsMovementObstructed, and its purpose is to detect if there is a destructible object in front of the AI character obstructing its movement. If that is true, the consideration returns a score based on the normalized distance between the AI character and the object, and also stores the object in a given blackboard key; otherwise, the consideration returns a score of 0.

Let's start by explaining the header file, following a similar order in which things are defined:

Most considerations inherit from UIAUSAxisInput_Range. This class provides two float instance variables, Minimum and Maximum, which are used to normalize the consideration's input and can be adjusted from the editor. More detail on that will be given at the end of the section. Considerations that do not need such normalization should instead extend the class UIAUSBTDecorator_Consideration.

#include "IAUS/Public/Considerations/IAUSAxisInput_Range.h"
UCLASS(Meta = (DisplayName = "Is Movement Obstructed Consideration", Category = "Considerations"))
class PROJECTBOREALIS_API UIAUSConsideration_IsMovementObstructed : public UIAUSAxisInput_Range
{
	...
};

A consideration needs a constructor in the following cases:

  • It inherits from UIAUSAxisInput_Range and has to set a default value for Minimum or Maximum, or
  • It has FBlackboardKeySelector instance variables and it is desired to filter the types of blackboard keys that the user can select from the editor.
public:
	UIAUSConsideration_IsMovementObstructed();

Then, if the consideration has to write or read values from the blackboard, it is necessary to override the function InitializeFromAsset and to declare the required blackboard key selector instance variables of type FBlackboardKeySelector.

public:
	virtual void InitializeFromAsset(UBehaviorTree& Asset) override;
private:
	/** Blackboard key where the object obstructing the pawn's movement will be stored */
	UPROPERTY(EditAnywhere, Category = Blackboard)
	FBlackboardKeySelector ObstructingObjectKey;

Finally and most importantly, all considerations must override the function Score that evaluates the Context passed as parameter and returns an appropriate score.

public:
	virtual float Score(const struct FIAUSBehaviorContext& Context) const override;

Now let's jump to the implementation file:

The constructor sets the default value of Maximum to 2000. All distance-based considerations in Project Borealis set the default value of Maximum to 2000 (for no reason in particular, I believe), and Minimum is left to 0, but they can be adjusted from the editor if necessary. Both values will be used in the Score function to normalize the distance between the AI character and the object obstructing its movement. In addition, the constructor filters the instance variable ObstructingObjectKey so that only blackboard keys of type APBWorldObjectPhysicsDestructible can be selected from the editor. Filtering key types is completely optional but is a good practice, specially if the blackboard has a large number of keys.

UIAUSConsideration_IsMovementObstructed::UIAUSConsideration_IsMovementObstructed()
{
	Maximum = 2000.0f;

	ObstructingObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UIAUSConsideration_IsMovementObstructed, ObstructingObjectKey),
										 APBWorldObjectPhysicsDestructible::StaticClass());
}

Then, as the name suggests, the function InitializeFromAsset initializes the blackboard keys for its correct use.

void UIAUSConsideration_IsMovementObstructed::InitializeFromAsset(UBehaviorTree& Asset)
{
	Super::InitializeFromAsset(Asset);

	const UBlackboardData* const BlackboardData = GetBlackboardAsset();
	if (ensure(BlackboardData))
	{
		ObstructingObjectKey.ResolveSelectedKey(*BlackboardData);
	}
}

Finally comes the implementation of the Score function, the heart of the consideration.

float UIAUSConsideration_IsMovementObstructed::Score(const FIAUSBehaviorContext& Context) const
{
	...
}

First, let's take a look a the definition of the Context passed as parameter:

struct FIAUSBehaviorContext
{
	class IAUSEvaluator* Evaluator;
	class AActor* Actor;
	class AAIController* AIController;
	float TotalScore;
	int32 BehaviorIndex;
};

From all these variables, only Actor and AIController will be used by Score. The rest are used by the internal implementation of IAUS and should be disregarded. It is important to understand that AIController is the controller of the AI character that is executing the Behavior Tree, while Actor is the character that IAUS is currently evaluating this behavior with. In this example, Break Obstacle (the behavior using this consideration) has the flag Target Hostile set, so Actor could be any of the characters with an enemy relationship to the AI character's own team (for example, the player).

It is important to validate the pointers that will be dereferenced inside the function. In particular, Context.Actor, Context.AIController, Context.AIController->GetPawn(), and GetWorld() could be invalid during level streaming, and Context.AIController->GetBlackboardComponent() could be invalid if the Behavior Tree is corrupted.

	const UWorld* const World = GetWorld();
	if (!IsValid(Context.AIController) || !IsValid(World))
	{
		return 0.0f;
	}

	APawn* const Pawn = Context.AIController->GetPawn();
	UBlackboardComponent* const BlackboardComponent = Context.AIController->GetBlackboardComponent();
	if (!IsValid(Pawn) || !IsValid(BlackboardComponent))
	{
		return 0.0f;
	}

Without going into unnecessary details, this function does a collision check to find all destructible objects that are in front of the AI character. Then, it does the following:

	...

	for (const FOverlapResult& OverlapResult : OverlapResults)
	{
		... // Filter out objects that do not meet certain conditions

		// An obstructing object was found
		BlackboardComponent->SetValueAsObject(ObstructingObjectKey.SelectedKeyName, DestructibleObject);

		const float Input = FVector::Distance(Pawn->GetActorLocation(), DestructibleObject->GetBlastMeshComponent()->GetChunkCenterWorldPosition(0));
		const float Normalized = (Input - Minimum) / (Maximum - Minimum);

		return ResponseCurve->ComputeValue(Normalized);
	}

	BlackboardComponent->SetValueAsObject(ObstructingObjectKey.SelectedKeyName, nullptr);

	return 0.0f;

Note that, if an obstructing object is found, it is stored in the blackboard key ObstructingObjectKey, the distance between the AI character and the other actor is computed and normalized using Minimum and Maximum, and that normalized input is used to compute and return the score using the consideration's response curve. Otherwise, ObstructingObjectKey is emptied and a score of 0 is returned.

Clone this wiki locally