Utility Reasoner

Utility reasoner allows you to use utility systems out of the box. To make it work you only need to provide blueprints mapped with evaluators. If you want to learn more about Utility AI, see the Utility page.

Utility Reasoner Types

Grail supports two ways of selecting behaviors based on their utility - max and roulette selection (see Utility Selectors page). Mirroring this, there are two types of utility reasoners, MaxUtilityReasoner and RouletteUtilityReasoner.

Persistence

Persistence is an optional parameter determining a score bonus for the currently executed behavior. If the entity has no assigned behavior, everything will be processed as usual. Otherwise, the current behavior will be evaluated again and its score will be multiplied by (1 + persistence). The purpose of persistence is to prevent erratic behavior by ensuring that entities swap behaviors only if new options are significantly better than the current one.

Operation Stack

While using this reasoner, suspended behaviors are put on top of the stack and can be retrieved later if they are still legal. Behaviors from the stack also get a persistence bonus to their score.

Example

If your character is running to a point of interest, but gets ambushed by enemies and decides to fight back the nearest one, the "run" behavior is suspended and put on top of the stack. When the character finishes fighting targeted enemy, if persistence is high enough, they will abort fighting other enemies and resume suspended "run" behavior. Otherwise, the bonus will be too small and the character will commit to fighting the rest of the enemies. However, if somehow "run" behavior gets delegalized (for example: the character ran out of stamina), it will be removed from the stack.

Roulette Options

RouletteUtilityReasoner has an additional constructor argument, containing the parameters for roulette selection (see the description here), defined as RouletteOptions structure. Its usage is shown in the code samples below.

Code Example

  • C++

  • C#

//Consideration
auto healthPercentageConsideration = std::make_shared<SimpleConsideration>();

//Curves
auto attackCurve = std::make_shared<grail::evaluator::LinearFunction<grail::EntityBlackboardPair>>(healthPercentageConsideration, 1, 0);
auto healCurve = std::make_shared<grail::evaluator::LinearFunction<grail::EntityBlackboardPair>>(healthPercentageConsideration, -1, 1);

//Blueprints - last 2 template arguments are skipped because they are defaulted to EntityBlackboardPair and AIEntity
grail::utility::Blueprint<grail::Behavior> fightBlueprint("fight", Fight::ProduceContext, Fight::ProduceInstance);
grail::utility::Blueprint<grail::Behavior> healBlueprint("heal", GetMedkit::ProduceContext, GetMedkit::ProduceInstance);

//Blueprints mapped with evaluators
std::vector<grail::utility::BlueprintOption<grail::Behavior, grail::EntityBlackboardPair, grail::AIEntity>> blueprintOptions
{
  {attackCurve, fightBlueprint, 0},
  {healCurve, healBlueprint, 0}
};

grail::AIManager manager{ 0 }; //manager without worker threads
auto maxReasonerEntity = std::make_shared<AIEntity>(); // constructed new entity
maxReasonerEntity->SetReasoner(std::make_unique<grail::utility::MaxUtilityReasoner>(std::move(blueprintOptions))); //assign constructed reasoner to entity
manager.RegisterAIEntity(maxReasonerEntity, 0); //register entity in manager

auto rouletteReasonerEntity = std::make_shared<AIEntity>();
auto rouletteOptions = grail::utility::roulette_selector::RouletteOptions().WithNumberOfTopBehaviors(10).WithRelativeUtilityThreshold(0.05f);
//MyRandomNumberGenerator inherits from RandomNumberGenerator
auto myRandomNumberGenerator = std::make_shared<MyRandomNumberGenerator>();
rouletteReasonerEntity->SetReasoner(std::make_unique<grail::utility::RouletteUtilityReasoner>(std::move(blueprintOptions), myRandomNumberGenerator, rouletteOptions));
manager.RegisterAIEntity(rouletteReasonerEntity, 0);

...

manager.UpdateReasoners(); //allow entities to query their reasoners
...
manager.UpdateEntities(delta); //allow entities to update their behaviors' state
//Consideration
var healthPercentageConsideration = new SimpleConsideration();

//Curves
var attackCurve = new LinearFunction(1, 0);
var healCurve = new LinearFunction(-1, 1);

//Blueprints
var fightBlueprint = new Blueprint<Behavior, EntityBlackboardPair, AIEntity>("fight", Fight.ProduceContext, Fight.ProduceInstance);
var healBlueprint = new Blueprint<Behavior, EntityBlackboardPair, AIEntity>("heal", GetMedkit.ProduceContext, GetMedkit.ProduceInstance);

//Blueprints mapped with evaluators
var blueprintOptions = new List<BlueprintOption<Behavior, EntityBlackboardPair, AIEntity>>()
{
  {attackEvaluator, fightBlueprint, 0},
  {healEvaluator, healBlueprint, 0}
};

var manager = new AIManager(0); //manager without worker threads
var maxReasonerEntity = new AIEntity() //constructed new entity
{
  Reasoner = new MaxUtilityReasoner(blueprintOptions) //assign constructed reasoner to entity
};

manager.RegisterAIEntity(maxReasonerEntity, 0); //register entity in manager

//MyRandomNumberGenerator inherits from RandomNumberGenerator
MyRandomNumberGenerator myRandomNumberGenerator = new();
RouletteOptions rouletteOptions = new()
{
  NumberOfTopBehaviors = 10,
  RelativeUtilityThreshold = 0.05f
};

var rouletteReasonerEntity = new AIEntity() //constructed new entity
{
  Reasoner = new RouletteUtilityReasoner(blueprintOptions, myRandomNumberGenerator, rouletteOptions)
};

manager.RegisterAIEntity(rouletteReasonerEntity, 0);
...

manager.UpdateReasoners(); //allow entities to query their reasoners
...
manager.UpdateEntities(delta); // allow entities to update their behaviors' state.