Becoming a sponsor will help this project stay alive, and make me a happy man :).
UAC consists of three parts
- A collection of highly performant Utility Ai algorithms
- A framework for building complex Utility Ais, with a minimum amount of coding
- A UI editor tool for Unity, which allows developers to effortlessly develop and maintain Utility Ais
Creating any Ai for a game will almost certainly be a cumbersome task. It requires intricate knowledge of the chosen Ai architecture and a modern Ai is among other things required to be:
- Performant
- Easily maintainable
- Modifiable
- Dynamic
- Customizable
The UAC comes out of the box with a set of highly performant decision making algorithms, and provides settings for ensuring a minimum FPS or restricting the Ai to a certain time budget.
The UAC allows a large amount of customization, through the use of inheritance.
- Unity Version 2021.2.10f1 or higher. Get newest version
Documentation in the form of Rich Pictures, UML, C4 and more can be found under Documentation
Sample code can be found in the UAC-Show Case repository
For support join: Support Chanel Discord
To install the Unity Utility AI System package, follow these steps:
- Open the Package Manager:
Window
->Package Manager
. - In the top-left corner of the Package Manager, click the
+
button and selectAdd package from git URL
. - Add one of the following links and press 'Add' Get help:
- HTTPS: https://github.com/DevOelgaard/UnityUtilityAiSystem.git
- SSH: git@github.com:DevOelgaard/UnityUtilityAiSystem.git (requires SSH key)
- The package is now installed and should appear in the Package Manager.
- If you don't see it, make sure to select the filter:
"Packages: In Project"
in the Package Manager toolbar.
- Create 2 C# scripts anywhere in your assets/scripts folder and name them "PrintTxt.cs" and "GameHandler.cs"
- Place the following code inside the "PrintTxt.cs" file
using UnityEngine;
public class PrintTxt: AgentAction
{
public PrintTxt()
{
// Add parameters in the constructor
AddParameter("Text to print","");
}
public override void OnStart(IAiContext context)
{
// Read the value of the parameter defined above. The parameterName is case sensitive
var textFromParameter = ParameterContainer.GetParamString("Text to print").Value;
// Get the agent from the Context and cast him as AgentMono
// Don't think to much about the casting to AgentMono, just do it ;)
var agent = context.Agent as AgentMono;
// Printing to the console
Debug.Log(agent.name +": " + textFromParameter);
}
}
- Place the following code inside the "GameHandler.cs"
using UnityEngine;
public class GameHandler : MonoBehaviour
{
private AgentMono agent;
// Start is called before the first frame update
void Start()
{
agent = GameObject.Find("Agent").GetComponent<AgentMono>();
}
// Update is called once per frame
void Update()
{
agent.ActivateNextAction();
}
}
- In the top toolbar of Unity: UAI Creator -> Template Manager
- If prompted press "Create New Project"
- Create an empty folder, name it "HelloWorld" and select it
- In the 'AIs' category: Add element -> Uai
- Double click the new Uai, which just appeared
- Name the UAI: 'Hello World'
- Add a bucket
- Expand the bucket by pressing Expand (On the left side of the newly added bucket)
- Add Consideration "Fixed Value"
- Select the Decisions Tab
- Add a decision
- Expand the decision by pressing Expand (On the left side of the newly added decision)
- Add Consideration "Fixed Value"
- Select the Actions Tab
- Add the Action "Print Txt". This is the action you implemented earlier
- Expand the "Print Txt" Action by pressing Expand
- In the Text to print field input: "Hello World". You defined this in code in the AddParameter method
- In the template managers toolbar: File -> Save
- In the top-right corner Toggle Playable on
- Close the Template manager
- Navigate to your scene view
- In the Hierarchy: Right Click -> 3D Object -> Cube
- Name it: "Agent"
- Select the Agent
- In the Agents Inspector: Add Component -> Agent Mono
- In the Agent Mono Component: Set the default Ai to Hello World.
- In the Hierarchy: Right Click -> Create Empty
- Name it: "GameHandler"
- In the GameHandlers Inspector: Add Component -> GameHandler
- In Unity's main window: Pres Play to run the game
- Watch the Console to see the Agent printing Hello World
Make the agent:
- Say: "Hi" the first time the action is selected
- Count the number of consecutive selections and print it to the console
- Change the PrintTxt.Cs file to look like this (Or ad a new file/class named something else):
using UnityEngine;
public class PrintTxt: AgentAction
{
public PrintTxt()
{
// Add parameters in the constructor
AddParameter("Text to print","");
}
// The OnStart method is called on the first selection
public override void OnStart(IAiContext context)
{
var agent = context.Agent as AgentMono;
// Printing to the console
Debug.Log(agent.name +": Hi");
}
// The OnGoing method is called on every consecutive selection
public override void OnGoing(IAiContext context)
{
// Read the value of the parameter defined above. The parameterName is case sensitive
var textFromParameter = ParameterContainer.GetParamString("Text to print").Value;
// Get the agent from the Context and cast him as AgentMono
// Don't think to much about the casting to AgentMono, just do it ;)
var agent = context.Agent as AgentMono;
// Printing to the console
Debug.Log(agent.name +": " + textFromParameter);
}
// The OnEnd method is called, when the decision was selected last evaluation, but not this evaluation
public override void OnEnd(IAiContext context)
{
var agent = context.Agent as AgentMono;
// Printing to the console
Debug.Log(agent.name +": Bye");
}
}
- Change the PrintTxt.Cs file to look like this (Or ad a new file/class named something else):
using UnityEngine;
public class PrintTxt: AgentAction
{
private int consecutiveSelectionsCounter = 0;
public PrintTxt()
{
// Add parameters in the constructor
AddParameter("Text to print","");
}
// The OnGoing method is called on every consecutive selection
public override void OnGoing(IAiContext context)
{
// Incrementing the counter.
consecutiveSelectionsCounter++;
// Printing to the console
Debug.Log("Consecutive Selections: " + consecutiveSelectionsCounter);
}
// The OnEnd method is called, when the decision was selected last evaluation, but not this evaluation
public override void OnEnd(IAiContext context)
{
// Resetting counter when, the action isn't selected
consecutiveSelectionsCounter = 0;
}
}
This chapter is meant to give you an overview of the UAI. You can read it all or skip to the sections representing the topics you wish to learn more about. After reading this chapter you will have a general understanding of, what was happening during the Hello World example.
A UAI is created by putting together multiple parts. At the top level you have the UAI, which consists of a collection of Buckets, each bucket holds a collection of Considerations and a collection of Decisions. Each decision holds a collection of Considerations and AgentActions.
UAI: Used by an agent as a container for all UAI objects, including decision making objects (more on that later).
Bucket: Collection for a group of similar Decisions, which shares one or more Considerations.
Decision: Holds AgentActions and Considerations relevant to those actions.
Consideration: Reads data from the game world and uses a response curve to convert it to a score between 0-1, where a higher score is interpreted as a higher usefulness for the agent.
AgentAction: Executes code in the attached system / game.
ResponseCurve: Used to model how data extracted with a consideration should be converted to a score.
The UAI consists of a collection of Buckets and 4 Decision Making Objects. By attaching a UAI to an agent, the agent will be able to select actions from within the UAIs buckets.
A bucket has a utility, a weight, a collection of Considerations and a collection of Decisions. The utility is calculated by the UAIs Utility Scorer and multiplied by the buckets weight. The decisions individual utilities are also multiplied by the buckets weight.
A bucket should be used to group all decisions which shares (or could share) the same considerations, for example all ranged attacks, could be placed in the same bucket called Ranged Attacks and a common consideration would be: “Do I have a ranged weapon”.
If any of the buckets considerations returns a score of 0, it indicates that all decisions in the bucket are invalid. I.e., if the agents don’t have a ranged weapon, he shouldn’t consider any ranged decisions.
The weight defines the priority of the bucket compared to other buckets, where a higher weight equals a higher priority, by increasing the likelihood of the buckets decisions being selected, (due to a higher total utility).
The utility represents how much the agent would benefit from choosing this decision. The rule for calculating the utility is defined by the Utility Scorer.
The Decision has a utility, a collection of Considerations and a collection of AgentActions. The score is calculated using the parent UAIs Utility Scorer and is multiplied by the parent Buckets weight.
A Decision represents a specific set of actions an agent can take, like “Fire bow at target”.
The Considerations should be relevant to the given decision. I.e., for the decision “Fire bow at target”, the considerations could be: “Chance to hit target”, “Potential damage to target” and “Desire to damage target”.
The utility represents how much the agent would benefit from choosing this decision. The rule for calculating the utility is defined by the Utility Scorer.
The actions define the code, that must be executed in the game. Int the “Fire bow at target” example, the actions could be: “Animate fire bow”, “Deal damage to target” and “Reduce ammunition”. Another approach could be to use a single action called: “Fire bow at target”, and let all logic concerning animation, damage, and ammunition be handled by the game, but this depends on the given use case.
using UnityEngine;
public class DecisionAllTargets:Decision
{
protected override float CalculateUtility(IAiContext context)
{
var bestUtility = float.MinValue;
GameObject bestTarget = null;
// Finding all targets with basic Unity method
var targets = GameObject.FindGameObjectsWithTag("Target");
foreach (var target in targets)
{
// Storing the target in the AI context and makes it available for all
// Child Considerations and AgentActions of the decision
context.SetContext("Target",target,this);
// Getting utility for current target
var utility = Evaluate(context);
if (utility > bestUtility)
{
bestTarget = target;
bestUtility = utility;
}
}
// Storing the best target to be used by AgentActions if the decision is selected
context.SetContext("Target",bestTarget,this);
// returning the best utility
return bestUtility;
}
}
The consideration can be considered the “Input” of the UAI, as this is the place, where data is extracted from the game and converted to a normalized score between 0-1.
Some basic considerations are shipped with the UAI Creator, but in almost all cases, some user defined considerations need to be implemented.
The consideration uses a Response Curve to convert the game data to a normalized score.
A score of 0 means that the consideration is impossible, and that the associated decision/bucket should be invalidated. An example of could be “Do I have a ranged weapon?”, and it could be attached to a bucket containing ranged attacks. If the agent does not have a ranged weapon, there is no reason to consider ranged attacks.
The performance tag is set by the user and is used by the UAI Creator to determine the order of execution of considerations. For performance reasons it is desirable to calculate performance-light considerations before performance-heave considerations.
There are multiple variants of considerations. The description above applies to all types of considerations, except as stated below.
- Modifier (Derived class)
- Used to change the weight of the parent decision/bucket.
The returned score is not used for calculating the parents’ score.
Example use case: The agent the decision “Heal Self”, which is placed in a combat bucket with a weight of 5. If the agents’ health drops below 5%, the “Heal Self” decision should be considered as life saving instead of basic combat. So, the Consideration-Modifier returns a score of 9 effectively raising the weight of the “Heal Self” decision.
- Used to change the weight of the parent decision/bucket.
- Boolean (Derived class)
- Used for simple questions which could invalidate the decision.
The returned score is not used for calculating the parents’ score.
The evaluation of consideration-booleans, are prioritized over basic considerations with the same performance tag, because these considerations have a high chance of invalidating the decision, thereby saving CPU time, by not evaluating redundant considerations.
Consideration-booleans aren’t part of the parents score because their success value is always 1, which means, that a decision with many Boolean considerations would score higher than intended.
An example is: “Do I have a Ranged Weapon?”, which has the potential to negate all ranged attack, but if the agent has a ranged weapon, doesn’t say anything about the agents’ utility of using the ranged weapon.
- Used for simple questions which could invalidate the decision.
- Setter (Attribute - Can be set on any type of consideration)
- Indicates, that the consideration saves data in the AI Context.
Setters must be evaluated before any other considerations since other considerations might depend on them.
A use case is: For an agent to move he must consider: “Clear path to target?”, “Time to travel path”, and “Threats on the path”. Individually these considerations would need to calculate the path to the target. Which probably is a performance heavy operation. To avoid this the “Clear path to target?” considerations, finds the path, and stores it in the AI Context, for the other considerations to use.
- Indicates, that the consideration saves data in the AI Context.
When implementing a consideration, the CalculateBaseScore method should be overridden. This method should return a float, representing the considered game data. The response curve will then convert the returned value to a normalized score.
Other methods to implement could be: BaseScoreBelowMinValue and BaseScoreAboveMaxValue. These defines, what the consideration should return if the game data was outside the defined range.
Sample code can be found in UAC-Show Case
The Response Curve is used to define how game data should be converted.
In the example on the screenshot of the Response Curve UI, an agent has an automatic rifle. At close range the rifle shouldn’t be used and returns a value of 0. The rifle is most effective at medium range, which is represented by the normal distribution between X=0.1 and X=0.8. Finally, the rifle has some special ability, which is very effective at long ranges.
You can create your own response functions by creating a class, that inherits from ResponseFunction.
The agent action can be considered the “Output” of the UAI.
The UAI Creator comes with some Unity specific Agent Actions, but in most cases, they should be implemented by the user.
The following functions should be used, when implementing an agent action.
- OnStart – Executed the first time an action is selected.
- OnGoing – Executed on each consecutive selection.
- OnEnd – Executed when the action was selected during last tick, but not selected this tick.
This evaluation | Last evaluation | Method |
---|---|---|
Selected | Not | OnStart |
Selected | Selected | OnGoing |
Not | Selected | OnEnd |
Sample code can be found in UAC-Show Case
Used to share data between UAI Objects.
All UAI Objects has read/write access to the context.
The relevant fields and methods are:
- object GetContext(object key, AiObjectModel requester), the key specifies, which data to access. The requester is the requesting UAI Object. If the requester is null, data will be searched from global available data. If the requester is a UAI Object, data will be searched in the context of the requester, all its ancestors and globally.
- Void SetContext(object key, object value, AiObjectModel owner), sets a value for a given key, and specifies the owner of the data. If no owner is set the data is stored globally. Define the owner when you want the data to be shared between descendants.
- LastSelectedDecision, LastSelectedBucket, CurrentEvaluatedDecision and CurrentEvaluatedBucket are used to access the bucket and decision who “won”, the last tick, and the bucket and decision, which is currently being evaluated.
{
var dataToShare = "Some data";
// Storing data for sharing
context.SetContext("SOME KEY",dataToShare,Parent);
// Retrieving data
var retrievedData = context.GetContext<string>("SOME KEY", this);
}
Sample code can be found in UAC-Show Case
The AgentMono component allows monobehaviours to use the UAI. Attach it to a monobehaviour through the Unity Inspector and set the settings as you wish.
The settings are:
- Default Ai: Sets the default AI to be selected on game start (Identified by name). If an error occurs, and the UAI can’t be found at game start, the first playable UAI will be selected.
- Auto Tick: If enabled the agent automatically makes decisions. This should be used for most games, that isn’t turn based or requires an agent to only act as a response to an event.
- Ms Between Ticks: Set the number of milliseconds that must have passed before the next decision.
- Frames Between Ticks: Set the number of frames that must have passed before the next decision.
The Ms Between Ticks and Frames Between Ticks are equal to a logical AND, meaning, that both must be true before a new Tick can be selected.
Use the … between ticks, settings to improve performance, by limiting the resources spent on decision making for an agent.
This is very use case specific. In an FPS or racing game the user might expect the agents, to react instantly to the users’ inputs, and these should be set very low. In other games it might seem weird, if all agents act instantaneously to changes in the game world.
The old Parameters.cs has been deprecated.
Description for the new parameter types must be filled here. Until then you refer to the coding examples
Multiple Types of parameters exists:
- ParamBool
- ParamColor
- ParamEnum
- ParamFloat
- ParamInt
- ParamString
Add parameters in the constructor of the desired AiObjectModel (Uai, Bucket, Decision, Consideration, AgentAction, Response Function).
Get the parameters as needed.
using UnityEngine;
public class Demo_AllParametersAction: AgentAction
{
public Demo_AllParametersAction()
{
// Parameters must be added in the constructor
AddParameter("Bool", true);
AddParameter("Enum",PerformanceTag.High);
AddParameter("Float",1f);
AddParameter("Int",100);
AddParameter("String","Hi you");
AddParameter("Color",Color.blue);
}
public override void OnStart(IAiContext context)
{
var b = ParameterContainer.GetParamBool("Bool");
var c = ParameterContainer.GetParamColor("Color");
var e = ParameterContainer.GetParamEnum("Enum");
var f = ParameterContainer.GetParamFloat("Float");
var i = ParameterContainer.GetParamInt("Int");
var s = ParameterContainer.GetParamString("String");
}
}
Decision Making Objects, references the objects, which in union handles the decision-making logic.
The DSE is the main DMO and serves as an interface to the underlying DMOs. The DSE is responsible for the continuous evaluation, until the best set of Agent Actions has been found. In most cases you should forget about this one and leave it as it is.
Any UAI has two UCS’s one for selecting buckets (Bucket Selector) and one for selecting decisions (Decision Selector). They can be changed from the UI (Template Manager).
There are many ways to select the next utility container (bucket or decision). The UAI Creator comes with the following UCS options:
- Highest Score: Selects the highest scoring utility container.
- Random X Highest: Select a random utility container among the X highest scorers. Can be set to select a random utility, where the chance of being selected is relative to the score (Percentage chance). Can set a max allowed deviation from the highest scorer, so only decisions which appears logical to the user is considered.
You can add your own UCS by creating a class that inherits from UtilityContainerSelector and implementing the abstract methods.
Is responsible for calculating the utility from a set of considerations. Currently the UAI Creator comes with the following utility scorers:
- Average scorer: Returns the average score of all considerations for a given utility container.
- Highest score: Returns the highest score of all considerations for a given utility container.
- Lowest score: Returns the lowest score of all considerations for a given utility container.
You can add your own Utility Scorer by creating a class, that implements the IUtilityScorer, but remember: - Return 0 if any consideration returns <=0.
- Only use the score from considerations which are scorers
- Use Consideration.IsScorer to check this. (See advanced guide)
Becoming a sponsor will help this project stay alive, and make me a happy man :).