Behavior Instantiation

Commonly, utility systems evaluate and select only the behavior type. Then it is parametrized with heuristically chosen data.

Example 1:

Behaviors types
  • Attack

  • StayIdle

Characters
  • AI character

  • troll - strong and far from AI character

  • goblin - weak and close to AI character

Let’s assume that AI character has chosen Attack action. It is now chosen that AI character should attack goblin because of user provided heuristic saying that closest enemy should by attacked first.

Grail allows for a more clever usage of the technique. Utility Reasoner allows users to evaluate and select Contexts - abstract data which will allow you to instantiate proper instances of behaviors.

Example 2:

Considering behavior types and Characters from Example 1 we instantiate all applicable behaviors.

So list of all behaviors appear as follows
  • Attack(troll)

  • Attack(goblin)

  • StayIdle

Each Attack behavior can be evaluated independently. This gives user more conflict resolving abilities during selection.

If we provide the playing model with curves saying that character should attack closer enemies, Attack(goblin) will be chosen. On the other hand if curves decide that stronger enemies should be attacked with priority, the playing model will select Attack(troll).

To make it work, the user should implement ProduceContexts function for Attack behavior.

There should be two possible Attack Contexts:
  1. troll

  2. goblin

User should also implement ProduceInstance functionality which will return behavior instance given a selected behavior. So given any enemy (in our case - troll and goblin) it should give us an instance of Attack(enemy).

Those two functions should be passed to blueprint object.

StayIdle may have an empty contexts because it doesn’t affect anything other than entity executing this behavior.

Easiest way to properly configure blueprints is to use a blackboard as a part of Context. That way each behavior can use as many different parameters as user needs as long as they are written to this blackboard.

After doing so, all blueprints must be passed to Utility Reasoner (or Utility Selector if implementing custom Reasoner).

Example Implementation

  • C++

  • C#

class Attack : public grail::Behavior
{
public:
  Attack(int damage)
    : damage{damage}
  {
  }

  Attack(int damage, Enemy* target)
    : Attack(damage), target{target}
  {
  }

  virtual ~Attack() override = default;
  Attack(const Attack& other) = default;
  Attack& operator = (const Attack& other) = default;

  void Start(const grail::AIEntity& entity) override
  {
    target->Attack(damage); //executed on assigning behavior
  }

  bool IsFinished(const AIEntity& entity) override
  {
    return true; //instantly finished behavior
  }

  bool IsLegal(const grail::AIEntity& entity) override
  {
    return target->health > 0; //legal if enemy is alive
  }

  static std::vector<grail::Blackboard> ProduceContexts(const grail::AIEntity& entity)
  {
    std::vector<grail::Blackboard> contexts;
    auto enemies = entity.GetBlackboard().GetValue<std::vector<Enemy>>("enemies");
    for(Enemy& enemy : enemies)
    {
      grail::Blackboard blackboard{};
      blackboard.SetValue("target", &enemy);
      contexts.push_back(blackboard);
    }
    return contexts;
  }

  static std::unique_ptr<grail::Behavior> ProduceInstance(const grail::Blackboard& blackboard)
  {
    return std::make_unique<Attack>(damage, blackboard.GetValue<Enemy>("target"));
  }

private:
  int damage;
  Enemy* target;
};
...

grail::Blueprint<grail::Behavior, grail::Blackboard, grail::AIEntity> blueprint{"attack", Attack::ProduceContexts, Attack::ProduceInstance};
class Attack : Grail.Behavior
{
  private int damage;
  private Enemy target;

  public Attack(int damage)
  {
    this.damage = damage;
  }

  public Attack(int damage, Enemy target)
    : this(damage)
  {
    this.target = target;
  }

  public override void Start(Grail.AIEntity entity)
  {
    target.Attack(damage); //executed on assigning behavior
  }

  public override bool IsFinished(Grail.AIEntity entity)
  {
    return true; //instantly finished behavior
  }

  public override bool IsLegal(Grail.AIEntity entity)
  {
    return target.health > 0; //legal if enemy is alive
  }

  public static List<Grail.Blackboard> ProduceContexts(in Grail.AIEntity entity)
  {
    List<Grail.Blackboard> contexts = new List<Grail.Blackboard>();
    auto enemies = entity.Blackboard.GetValue<List<Enemy>>("enemies");
    for(Enemy enemy : enemies)
    {
      Grail.Blackboard blackboard = new Grail.Blackboard();
      blackboard.SetValue("target", enemy);
      contexts.Add(blackboard);
    }
    return contexts;
  }

  public static Grail.Behavior ProduceInstance(Grail.Blackboard blackboard)
  {
    return new Attack(damage, blackboard.GetValue<Enemy>("target"));
  }
}
...

var blueprint = new Grail.Blueprint<Grail.Behavior, Grail.Blackboard, Grail.AIEntity>("attack", Attack.ProduceContexts, Attack.ProduceInstance);