DomainTranslator

DomainTranslator is the class that’s an intermediary between the 'real' game state and the planner state space. It is used to define the plan domain (action templates and object types), to create a binding between in-game objects and planner World Objects and - finally - to translate abstract plans to behavior sequences.

Important methods

TranslatePlan

The planner returns a Planner.AbstractPlan — an abstract representation of a sequence of actions.

With this abstract method, you can take the abstract plan and translate it into Grail’s Plan, which is a sequence of Behaviors.

Keep in mind that a single action from the planning domain does not have to correspond to a single behavior — your plan translation logic can be arbitrary.

GeneratePlannerObject

This method should return a vector of IPlannerObject instances that correspond to in-game objects.

For each instance, a binding will be created between the abstract object and the in-game object.

Please note that, although this distinction is typically useful, convenient, and potentially allows for faster implementation, you are not prohibited from using the same objects for both planning and regular game logic.

CreateObjectTypes

This method should return all possible WorldObjectType types used by the planner.

AddActionTemplates

Inside this method, action templates for all possible actions should be added to the Domain. See the Example implementation.

SetWorldStateParameters

Inside this method, initial abstract WorldState parameters should be set. Use state.SetParameter.

IPlannerObject

Grail provides an interface that can be implemented by in-game objects (or wrappers implemented specifically for AI). This interface is called IPlannerObject. It is used by the domain translator to create bindings between 'real' and abstract (i.e., those belonging to abstract state space).

The role of IPlannerObject is to produce a WorldObject associated with the original object. Here’s an example implementation:

  • C++

  • C#

class Item : public grail::planner::IPlannerObject
{
private:
  std::string name;
public:
  Item(const std::string& name) : name(name) {}

  virtual grail::planner::WorldObject ToWorldObject(grail::planner::Domain& domain, const grail::planner::ObjectIndexBinding&) const override
  {
    grail::planner::WorldObject obj = domain.CreateObjectOfType("item");
    obj.SetParameter("name", name);
    return obj;
  }
};
public class Item : Grail.IPlannerObject
{
  private string name;

  public Item(const std::string& name)
  {
    this.name = name;
  }

  public Grail.WorldObject ToWorldObject(Grail.Domain domain, Grail.ObjectIndexBinding binding)
  {
    WorldObject obj = domain.CreateObjectOfType("item");
    obj.SetParameter("name", name);
    return obj;
  }
};

Example implementation

Now that we have our IPlannerObject sorted out, we can proceed with the translator. Here’s an example implementation of a complete domain translator:

  • C++

  • C#

class MyTranslator : public grail::planner::DomainTranslator
{
public:
  MyTranslator(grail::planner::MemoryPool& memory)
    : grail::DomainTranslator{ memory }
  {
  }

  // Inherited via DomainTranslator
  virtual std::vector<grail::planner::IPlannerObject*> GeneratePlannerObjects(grail::AIEntity& entity, const grail::planner::Goal&) override
  {
    //In this method we assume that Item class implements IPlannerObject interface
    //the returned vector will be used to create the initial world state
    //and create a mapping between real in-game objects and abstract planner objects

    std::vector<grail::planner::IPlannerObject*> result;

    //A list of considered items is stored on the entity's blackboard
    auto worldItems = entity.GetBlackboard().GetValue<std::vector<std::shared_ptr<Item>>>("worldItems");
    for (auto& item : worldItems)
    {
      result.push_back(item.get());
    }

    //more object types follow
    //...

    return result;
  }

  virtual std::vector<grail::planner::WorldObjectType> CreateObjectTypes(const grail::AIEntity&, const grail::planner::Goal&) override
  {
    std::vector<grail::planner::WorldObjectType> result;
    result.push_back(grail::planner::WorldObjectType(memory, "item"));
    //... more item types follow
    return result;
  }

  virtual void AddActionTemplates(const grail::AIEntity&, const std::shared_ptr<grail::planner::Domain>& inDomain, const grail::planner::Goal&) override
  {
    //this action accepts one argument of type "item"
    auto& action = inDomain->AddActionTemplate("pick_up", { "item" });
    action.SetPreconditions([](const std::vector<const grail::planner::WorldObject*>& args, const grail::planner::WorldState&)
    {
      return true;
    });

    action.SetEffects([](const std::vector<grail::planner::WorldObject*>& args, grail::planner::WorldState& state)
    {
      std::string itemName = args[0]->GetParameter<string>("name");
      int itemCount = state.GetParameter<int>(itemName);
      state.SetParameter(itemName, itemCount + 1);
      state.RemoveObject(args[0]->GetObjectIndex());
    });

    //more actions follow
    //...
  }

  virtual grail::Plan TranslatePlan(const grail::AIEntity&, const grail::planner::Planner::AbstractPlan& plan, const grail::planner::Goal&) const override
  {
    //when translating the plan, we use action names to identify
    //needed behaviors and created index-to-object binding to recover in-game objects
    grail::Plan result;
    for (const auto& action : plan.actions)
    {
      if (action.GetName() == "pick_up")
      {
        auto item = binding.GetObjectByIndex<Item>(action.GetArgumentIndices()[0]);
        result.PushBehavior(std::make_unique<GoTo>(item));
        result.PushBehavior(std::make_unique<PickUp>(item));
      }

      //... more actions can be interpreted in a similar fashion
    }
    return result;
  }
};
public class MyTranslator : public Grail.DomainTranslator
{
  // Inherited via DomainTranslator
  public override List<Grail.IPlannerObject> GeneratePlannerObjects(Grail.AIEntity entity, Grail.Goal goal)
  {
    //In this method we assume that Item class implements IPlannerObject interface
    //the returned vector will be used to create the initial world state
    //and create a mapping between real in-game objects and abstract planner objects

    var result = new List<Grail.IPlannerObject>();

    //A list of considered items is stored on the entity's blackboard
    var worldItems = entity.Blackboard.GetValue<List<Item>>("worldItems");
    foreach (var item in worldItems)
    {
      result.Add(item);
    }

    //more object types follow
    //...

    return result;
  }

  public override List<Grail.WorldObjectType> CreateObjectTypes(Grail.AIEntity entity, Grail.Goal goal)
  {
    var result = new List<Grail.WorldObjectType>();
    result.Add(new Grail.WorldObjectType("item"));
    //more item types follow
    //...
    return result;
  }

  public override void AddActionTemplates(Grail.AIEntity entity, Grail.Domain inDomain, Grail.Goal goal)
  {
    //this action accepts one argument of type "item"
    var action = inDomain.AddActionTemplate("pick_up", new List<string>() { "item" });
    action.Preconditions = (args, state) =>
    {
      return true;
    };

    action.Effects = (args, state) =>
    {
      string itemName = args[0].GetParameter<string>("name");
      int itemCount = state.GetParameter<int>(itemName);
      state.SetParameter(itemName, itemCount + 1);
      state.RemoveObject(args[0].ObjectIndex);
    };

    //more actions follow
    //...
  }

  public override Grail.Plan TranslatePlan(Grail.AIEntity entity, Grail.Planner.AbstractPlan plan, Grail.Goal goal)
  {
    //when translating the plan, we use action names to identify
    //needed behaviors and created index-to-object binding to recover in-game objects
    Grail.Plan result = new Grail.Plan();
    foreach (var action : plan.actions)
    {
      if (action.Name == "pick_up")
      {
        auto item = binding.GetObjectByIndex<Item>(action.ArgumentIndices[0]);

        //as you can see, one abstract action can correspond to more than one behavior
        result.PushBehavior(new GoTo(item));
        result.PushBehavior(new PickUp(item));
      }

      //... more actions can be interpreted in a similar fashion
    }
    return result;
  }
};