Debugging Setup: Tutorial

This is a short tutorial on how to prepare your game for debugging.

Before you start

  • Please make sure you are familiar with the basics of the algorithms in order to debug them:

  • We assume that you already have an AIEntity that has a reasoner attached. If you want to revisit how to create a Reasoner, here are the pages:

  • The goal of this tutorial is to show an example of making the game gather debug data from the reasoner so the debugger tool can access it.

Debugging always comes with a performance overhead, especially in the case of Simulated Games or Planners. Make sure to disable it in the final release version of the game.

Much of the debugging code is similar regardless of the reasoner used. The main difference is how the internal data is serialized and the following sections are devoted to specifics of the respective algorithm.

Entity Data - serialization code for blackboards

To properly serialize custom data structures stored in blackboards, you have to provide some conversion logic.

  • C++

  • C#

#include <grail/Blackboard.hh>

class MyType
{
public:
  float floatField;
}

std::string MyTypeToString(const MyType& obj)
{
  return "floatField: " + std::to_string(obj.floatField);
}

BLACKBOARD_PARAM_CONVERSION(MyType, MyTypeToString)
public class MyType
{
  public float floatField;
  public override string ToString()
  {
    return "floatField: " + floatField;
  }
}

Simulated Games - write serialization code for your actions and units

Actions

Implement the ToString method:

  • C++

  • C#

//ChooseDieAction
std::string ChooseDieAction::ToString() const
{
  return "Choose [" + ChosenDie->ToString() + "]";
}

//RollAction
std::string RollAction::ToString() const
{
  return "Roll [" + std::to_string(dieValue) + "]";
}

//BuyWeaponAction
std::string BuyWeaponAction::ToString() const
{
  return "Buy weapon [" + std::to_string(weaponAttackValue) + "]";
}
//ChooseDieAction
public override string ToString() => $"Choose [{ChosenDie}]";

//RollAction
 public override string ToString() => $"Roll [{DieValue}]";

//BuyWeaponAction
public override string ToString() => $"Buy weapon [{WeaponAttackValue}]";

Units

Implement the ToString and FillDebugRepresentation methods:

  • C++

  • C#

//Player
std::string Player::ToString() const
{
  return "Player";
}

void Player::FillDebugRepresentation(std::map<std::string, std::string>& nameValueDictionary) const
{
  nameValueDictionary["State"] = std::to_string(static_cast<int>(state));
  nameValueDictionary["Money"] = std::to_string(money);
  nameValueDictionary["Attack"] = std::to_string(weaponAttackValue);
}

//DiePlayer
std::string DiePlayer::ToString() const
{
  return "DiePlayer [" + std::to_string(sideCount) + "]";
}
//Player
public override string ToString() => $"Player";

public override void FillDebugRepresentation(Dictionary<string, string> nameValueDictionary)
{
  nameValueDictionary.Add("State", State.ToString());
  nameValueDictionary.Add("Money", Money.ToString());
  nameValueDictionary.Add("Attack", WeaponAttackValue.ToString());
}

//DiePlayer
public override string ToString() => $"DiePlayer [{RollActions.Length}]";

Planners - serialization of planner objects and actions

The debugger gathers data about the current planning state represented by WorldObjects and actions taken represented by Action. These data is serialized and can be viewed in the GUI debugging tool for planners. You should make sure that these objects description is sufficient for your needs. Below we explain how the serialization mechanism works for them.

WorldObjects

The debug information consists of the name of the object and its parameters (properties). Revisit the section World Object.

  • If you defined the parameter called name then it will be the serialized name

  • If you did not define the parameter called name then the type name will be serialized as name

  • The parameters are serialized using the SerializeForGUI method of WorldObject class. You can override it.

The default implementation of the SerializeForGUI method is prepared only in C#.

However, in C++ in order not to force inheritance from WorldObject (and leave it optional) you have two options:

  • Provide a lambda function via SetSerializeForGUIFunction

  • Inherit from WorldObject and override SerializeForGUI.

  • C++

  • C#

//The default implementation calls a function
void WorldObject::SerializeForGUI(std::map<std::string, std::string>& state) const
{
  if (serializeGUIFunction != nullptr)
    serializeGUIFunction(*this, state);
}

//You can set this function here in order to avoid inheritance
void WorldObject::SetSerializeForGUIFunction(std::function<void(const WorldObject& object, std::map<std::string, std::string>&)> function)
{
  serializeGUIFunction = function;
}

//Example of usage:
grail::planning::WorldObject obj = domain.CreateObjectOfType("item");
obj.SetParameter("name", name);
obj.SetSerializeForGUIFunction([](const grail::planning::WorldObject& object, std::map<std::string, std::string>& state)
{
  state["name"] = object.GetParameterValue<std::string>("name");
});
//WorldObject.cs
public virtual void SerializeForGUI(Dictionary<string, string> state)
{
  foreach(var entry in parameters)
  {
    state.Add(entry.Key, entry.Value+"");
  }
}

Actions

The actions are serialized to the debugger with:

  • Name which is provided by the the action template and is required in the constructor.

  • Action’s arguments which are WorldObjects. The serialization of WorldObjects has already been covered.

You may want to revisit the section Adding Action Template to see how actions are represented.

Utility

The Utility System uses predominantly common engine components so the serialization requires significantly less work than in the case of Planners or Simulated Games. You only need to provide names for Behaviors, Blueprints used for behaviors and Considerations.

  • C++

  • C#

//Consideration:
const std::string GetDisplayName() const
{
    return "unnamed_consideration";
}

//Behavior:
virtual std::string GetName() const;

//Blueprint:
//(it is actually required to pass the name in blueprint constructor)
Blueprint(std::string name,
       std::function<std::vector<ContexType>(const DataType&)> contextProducer,
       std::function<std::unique_ptr<InstanceType>(const ContexType&)> instanceProducer)
      : name{name}, contextProducer{contextProducer}, instanceProducer{instanceProducer}
{

}
//Consideration:
public virtual string ToString();

//Behavior:
public virtual string ToString();

//Blueprint:
//(it is actually required to pass the name in blueprint constructor)
public Blueprint(string name, ContextProducerDelegate contextProducer, InstanceProducerDelegate instanceProducer)
{
    this.name = name;
    this.contextProducer = contextProducer;
    this.instanceProducer = instanceProducer;
}

In addition, the data present in the blackboard returned by the blueprint’s contextProducer will be serialized for you.

  • C++

  • C#

//Example - assume that this behavior is used by Utility AI
//The last 2 template arguments of blueprint are skipped because they are defaulted to EntityBlackboardPair and AIEntity
//The data from Blackboard from EntityBlackboardPair returned by Fight::ProduceContext will be serialized to the GUI tool
grail::Blueprint<grail::Behavior> fightBlueprint("fight", Fight::ProduceContext, Fight::ProduceInstance);
//Example - assume that this behavior is used by Utility AI
//The data from Blackboard from EntityBlackboardPair returned by Fight::ProduceContext will be serialized to the GUI tool
var fightBlueprint = new Blueprint<Behavior, EntityBlackboardPair, AIEntity>("fight", Fight.ProduceContext, Fight.ProduceInstance);
The above blackboard is a good place to store per-instance data for you behaviors used in Utility AI. Such data can be, for example, the target for your fight method.

In future versions, we might provide more customization of the Utility System. In such a case, we would add information how to serialize user-defined custom objects.

Provide time stamps

From now on, the code will be comon to all reasoners.
If you’re using Grail plugin for Unreal 4 or Unity, all the work described below is already done for you in the plugin’s code

The debug tools are equipped with a timeline. You can slide over it and investigate what happened at a particular time. Snapshots gathered by the debugger have timestamps assigned by the ITimestampProvider:

The interface is very simple:

  • C++

  • C#

class ITimestampProvider
{
public:
  ITimestampProvider() = default;
  ITimestampProvider(const ITimestampProvider&) = delete;
  ITimestampProvider(ITimestampProvider&&) = delete;
  ITimestampProvider& operator=(const ITimestampProvider&) = delete;
  ITimestampProvider& operator=(ITimestampProvider&&) = delete;
  virtual ~ITimestampProvider() = default;

  virtual float GetTimestamp() = 0;
};
public interface ITimestampProvider
{
  float Timestamp { get; }
}

The timestamps are provided by an object that implements this interface. Its purpose is to make the debugging process more convenient. If we made the implementation return a fixed value (0, for instance) then all debug information would at one place in the timeline.

In most cases, it is perfectly enough to implement the interface as simply as possible. Just return the game virtual time or real time (e.g. seconds from start):

  • C++

  • C#

class MockTimestampProvider : public grail::ITimestampProvider
{
public:
  float timestamp = 0;
  float GetTimestamp() override
  {
    return timestamp;
  }
};
class MockTimestampProvider : ITimestampProvider
{
  public float Timestamp { get; set; }
}

Create GrailDebugger and DebugInfoGenerator

  • GrailDebugger is a class that manages the process of debugging

  • DebugInfoGenerator is a class that gathers data and stores it for possible serialization

  • C++

  • C#

auto timestampProvider = std::make_shared<MockTimestampProvider>();

//create GrailDebugger and DebugInfoGenerator
grail::GrailDebugger debugger{ timestampProvider };
grail::DebugInfoGenerator debugInfoGenerator{ timestampProvider };

//add debugInfoGenerator to the debugger so it can store data
debugger.AddDebugSnapshotReceiver(&debugInfoGenerator);
var timestampProvider = new MockTimestampProvider();

//create GrailDebugger and DebugInfoGenerator
var debugger = new Grail.DebugInfo.GrailDebugger(timestampProvider);
var debugInfoGenerator = new Grail.DebugInfo.DebugInfoGenerator(timestampProvider);

//add debugInfoGenerator to the debugger so it can store data
debugger.AddDebugSnapshotReceiver(debugInfoGenerator);

Setup live debugging

In case you wish to use Grail’s live debugging feature, there is a small amount additional code you have to write.

  • C++

  • C#

//create live debugger server and register it to your debugger object
grail::LiveDebuggerServer liveDebuggerServer;
debugger.AddDebugSnapshotReceiver(&liveDebuggerServer);
//create live debugger server and register it to your debugger object
var liveDebuggerServer = new GrailDebugInfoServer("Grail Debug");
grailDebugger.AddDebugSnapshotReceiver(liveDebuggerServer);
//remember to call liveDebuggerServer.Dispose() after you stop using it

Enable debugging in AIManger

You enable debugging by attaching the debugger to the concrete AIManager. The second argument of the AttachToManager function is a boolean variable with the default value of false. If true, then the debugger will be attached to all entities that are managed by the AIManager.

If false, then you have to attach the debugger to chosen entities manually. Attaching to all entities is convenient, but you may prefer to debug only selected entities for many reasons e.g. for performance considerations or you just want to focus on a particular entity at once.

  • C++

  • C#

debugger.AttachToManager(&manager, true);

//OR
debugger.AttachToManager(&manager);
debugger.AttachToEntity(*aiEntity);
debugger.AttachToManager(manager, true);

//OR
debugger.AttachToManager(manager);
debugger.AttachToEntity(aiEntity);

Let the game run

Now we are ready and set to debug. Just let the game run as long as required.

Access or serialize

You can access all debug data via debugInfoGenerator.DebugInfo (C#) or debugInfoGenerator.GetDebugInfo() (C++). This object contains all data from all entities and reasoners that the debugger was attached to. However, in order to serialize the data for the GUI Debug Tool, just can just use the following code:

  • C++

  • C#

const auto filename = "simGameDebug.gdi";
c4::yml::writeToFile(filename, debugInfoGenerator.GetDebugInfo());
const string filename = "simGameDebug.gdi";
var serializer = new YamlSerializer();
using (StreamWriter writer = new StreamWriter(filename))
{
  serializer.Serialize(debugInfoGenerator.DebugInfo, writer);
}

Full simple example

We will use SimulatedGameReasoner as the example.

  • C++

  • C#

//AI setup:
grail::AIManager manager(0);
auto aiEntity = std::make_shared<grail::AIEntity>();
manager.RegisterAIEntity(aiEntity, 0);

auto reasoner = std::make_unique<grail::SimulatedGameReasoner>(std::make_unique<GameStateTranslator>(), std::make_unique<ActionTranslator>(),
    1000, //iterations per tick
    6000); //iterations total
aiEntity->SetReasoner(std::move(reasoner));

//Debugging setup:
float deltaTime = 0.05f;
auto timestampProvider = std::make_shared<MockTimestampProvider>();
grail::GrailDebugger debugger{ timestampProvider };
grail::DebugInfoGenerator debugInfoGenerator{ timestampProvider };
debugger.AttachToManager(&manager, true);
debugger.AddDebugSnapshotReceiver(&debugInfoGenerator);
debugger.AttachToEntity(*aiEntity);

for (int i = 0; i < 7; ++i)
{
  manager.UpdateReasoners();
  manager.UpdateEntities(deltaTime);
  debugger.Update();
  timestampProvider->timestamp += deltaTime;
}

//The debugging is finished:
const auto filename = "simGameDebug.gdi";
c4::yml::writeToFile(filename, debugInfoGenerator.GetDebugInfo());
//AI setup:
var manager = new AIManager();
var aiEntity = new AIEntity();
manager.RegisterAIEntity(aiEntity, 0);

var stateTranslator = new GameStateTranslator();
var reasoner = new SimulatedGameReasoner(stateTranslator, new ActionTranslator(), 1000, 6000);
aiEntity.Reasoner = reasoner;

//Debugging setup:
float deltaTime = 0.05f;
var timestampProvider = new MockTimestampProvider();
var debugger = new Grail.DebugInfo.GrailDebugger(timestampProvider);
var debugInfoGenerator = new Grail.DebugInfo.DebugInfoGenerator(timestampProvider);
debugger.AttachToManager(manager);
debugger.AddDebugSnapshotReceiver(debugInfoGenerator);
debugger.AttachToEntity(aiEntity);

//The following code simulates the game loop for the purpose of this test:
for (int i = 0; i < 7; ++i)
{
  manager.UpdateReasoners();
  manager.UpdateEntities(deltaTime);
  debugger.Update();
  timestampProvider.Timestamp += deltaTime;
}

//The debugging is finished:
const string filename = "simGameDebug.gdi";
var serializer = new YamlSerializer();
using (StreamWriter writer = new StreamWriter(filename))
{
  serializer.Serialize(debugInfoGenerator.DebugInfo, writer);
}

Next step

Once the debug data is extracted from the game, you can view it in the debugger. The following page shows how to work with the GUI-based debugging tool.