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:
-
Here is the Simulated Games introduction page.
-
Here is the Planner introduction page.
-
Here is the Utility introduction page.
-
-
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 (see Blackboard Serialization for details).
-
C++
-
C#
#include <GrailCore/Blackboard.hh>
class MyType
{
public:
float floatField;
}
namespace grail
{
template<>
std::string SerializeData<CustomStruct>(const MyType& element)
{
return "floatField: " + std::to_string(obj.floatField);
}
}
public class MyType
{
public float floatField;
public override string ToString()
{
return "floatField: " + floatField;
}
}
Simulated Games - write serialization code for your actions and units
-
In this tutorial, we will use the exemplar game defined in the Simulated Games - quickstart.
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 are serialized and can be viewed in the GUI debugging tool for planners.
You should make sure that these objects descriptions are 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 ofWorldObject
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 overrideSerializeForGUI
.
-
C++
-
C#
using grail::planner::WorldObject;
//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:
WorldObject obj = domain.CreateObjectOfType("item");
obj.SetParameter("name", name);
obj.SetSerializeForGUIFunction([](const 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 ofWorldObjects
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)
grail::utility::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::utility::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 on 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 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 be at one place on 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 MyTimestampProvider : public grail::ITimestampProvider
{
public:
float timestamp = 0;
float GetTimestamp() override
{
return timestamp;
}
};
class MyTimestampProvider : 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
They can be both created by a call to CreateDefaultFor
static helper function, defined in GrailDebugger
.
-
C++
-
C#
grail::AIManager aiManager;
auto timestampProvider = std::make_shared<MyTimestampProvider>();
auto [debugger, debugInfoGenerator] =
grail::GrailDebugger::CreateDefaultFor(aiManager, timestampProvider);
var aiManager = new AIManager();
var timestampProvider = new MyTimestampProvider();
var (debugger, debugInfoGenerator) = GrailDebugger.CreateDefaultFor(aiManager, timestampProvider);
Setup live debugging
If you wish to use Grail’s live debugging feature, you will need to write a small amount of additional code.
-
C++
-
C#
//create live debugger server and register it to your debugger object
grail::live::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 a specific 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);
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";
debugInfoGenerator->SaveToFile(filename);
const string filename = "simGameDebug.gdi";
debugInfoGenerator.SaveToFile(filename);
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::simgames::SimulatedGameReasoner>(std::make_unique<MyGameStateTranslator>(), std::make_unique<MyActionTranslator>(),
1000, //iterations per tick
6000); //iterations total
aiEntity->SetReasoner(std::move(reasoner));
//Debugging setup:
float deltaTime = 0.05f;
auto timestampProvider = std::make_shared<MyTimestampProvider>();
auto [debugger, debugInfoGenerator] =
grail::GrailDebugger::CreateDefaultFor(manager, timestampProvider);
debugger->AttachToManager(&manager, attachToReasoners);
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";
debugger->SaveToFile(filename);
//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 MyTimestampProvider();
var (debugger, debugInfoGenerator) = GrailDebugger.CreateDefaultFor(manager, 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";
debugInfoGenerator.SaveToFile(filename);