-
Notifications
You must be signed in to change notification settings - Fork 45
Behaviour system
Botcraft has a built-in behaviour tree system. This page will explain the main points of this system. For a more in-depth explanation of behaviour trees, I recommend reading this article by Chris Simpson.
Basically, it allows to combine simple actions to form complex behaviours, shaped as trees (hence the name). Here is an example to compare the syntax between traditional code and the builder of the botcraft behaviour tree system:
Traditional | Behaviour tree |
---|---|
|
|
The tree can then just be set as the active one of any class deriving from TemplatedBehaviourClient
for the behaviour to be executed on a dedicated thread, without having to worry about concurrent task execution.
If you don't like the behaviour tree system, it's also totally possible to code your behaviour in a more traditional way in a function. Your tree will then just be composed of one leaf calling your function. There is also a SyncAction
function in TemplatedBehaviourClient
that allows you to call any leaf compatible function. Note that this will replace the current tree by a simple one with your leaf and wait for its execution to return: this is a blocking call.
A tree only defines a logic flow, when and which function should be executed. It can thus be shared between multiple clients/bots. Trees are applied to a context (in our case a client) to actually perform actions following the defined logic.
When compiled with BOTCRAFT_USE_OPENGL_GUI=ON and BOTCRAFT_USE_IMGUI=ON, a debugger window is automatically added to the GUI. It can be opened/closed at any time by pressing B. It is composed of two main components: a blackboard window and a tree view.
The blackboard window shows all values currently inside the blackboard in real-time. All Botcraft default tasks will save their parameters in the blackboard before running, allowing easier debugging using this window. However, its use is not limited to tasks. You could use it anywhere in your code to have a real-time display of any value during bot execution. Here are examples on how to set values in the blackboard:
blackboard.Set<int>("DEBUG.FOLDER.my_int_value", my_int_value);
blackboard.Set<short>("DEBUG.my_short_value", my_short_value);
blackboard.Set<MyCustomClass>("my_class", object_of_my_class);
As there is no standard way to convert any object to string in C++, you will need to register any non already present types at runtime. A list of already registered types can be found in StdAnyUtilities.cpp in the definition of registered_types
. Here is an example on how to register any custom type:
Botcraft::Utilities::AnyParser::RegisterType<MyCustomClass>([](const std::any& f) {
std::string output = "";
const MyCustomClass& v = std::any_cast<const MyCustomClass&>(f);
// Convert v to string and store it in output
return output;
});
Once registered, this function will be automatically called whenever the blackboard debugger needs to display a value of type MyCustomClass
.
You can navigate in the tree using right click and zoom by scrolling. Pressing F will alternatively focus on the currently hovered node or fit to see full tree.
Nodes are ticked from left to right, top to bottom. A green border on a node means the node returned Success, a red border means Failure, a yellow border means the node is currently being run (or one of its children) and a grey border means the node has not been reached yet.
Subtrees can be hidden from the view by clicking the output pin of the subtree root node's parent. A pause can never happen in a hidden subtree.
There are two buttons at the top of the window:
- the first one allows to pause/resume the behaviour (⚠ stopping can only happen at the beginning of a node, if pause is asked during a node execution, the behaviour will continue until the next node is reached). Behaviour is also paused when it reaches a selected node (orange border around the node). You can select nodes by left clicking them. Multiple selection is possible using Ctrl+left click. When paused, the behaviour can be resumed either by pressing the button or pressing F5.
- the second button can only be used when already paused. It will resume the behaviour and automatically pause it on the next visible node (step by step mode). This can also be achieved using F10 when paused.
To use the behaviour tree system, you have to use a class derived from BehaviourClient
, which means that you have a Blackboard
object available. This blackboard can be used to store and retrieve any kind of data, indexed by a string. You can use it to pass data from one leaf to another. This tree will for example send "Hello" then "World!" in the chat, the function SayBlackboard
reading the value stored at the location "Say.msg" in the blackboard.
auto behaviour = Builder<SimpleBehaviourClient>()
.sequence()
.leaf(SetBlackboardData<std::string>, "Say.msg", "Hello")
.leaf(SayBlackboard)
.leaf(SetBlackboardData<std::string>, "Say.msg", "World!")
.leaf(SayBlackboard)
.end();
Each leaf of the tree represents an action. Botcraft already has a number of prebuilt actions for basic stuff like pathfinding or digging, but you can also define your own custom ones. They can be any function (including lambdas and std::function) respecting these two conditions:
- the return type must be a Botcraft::Status (enum indicating Success or Failure of the action)
- the first argument must be a reference to an object deriving from
BehaviourClient
(that will be the application context of the tree)
These are all examples of valid leaf functions:
Status Success(BehaviourClient& client) { return Status::Success; }
Status Log(BehaviourClient& client, const std::string& msg) { std::cout << msg << std::endl; return Status::Success; }
auto foo = std::bind(Log, std::placeholders::_1, "foo");
auto bar = [=](SimpleBehaviourClient& client) { return Log(client, "bar"); }
A leaf doesn't have to be simple. In fact, a leaf can also call other functions. It's up to you to decide whether you want to have a very generic leaf that you can reuse multiple times in a tree, or a very specific one, that can replace a full part of a tree with a more traditional code style.
Some action like pathfinding or digging, are not performed instantly. In this case, the action can call the Yield
function. This will pause the behaviour thread until the next step. When this next step happens, the action will resume in the same state as before the Yield
call. Tree swapping or stopping can only happen during these pauses, so it's important to add some Yield
call in your action if they are not performed instantly and require some waiting (they will typically replace some std::this_thread::sleep_for
).
There are four components that can be used as nodes in a tree: tree, leaf, decorator and composite. A tree can be built using a Botcraft::Builder followed by a sequence of function calls to create subnodes. The constructor of Botcraft::Builder takes an optional string as parameter. If specified, it will be used as the tree name in the behaviour visualizer window as well as in the logged stack trace in case an exception happens during this tree execution.
A whole tree can be reused as a node in another one by calling .tree()
in the builder. The subtree and the main tree must share the same context type (you can't add a BehaviourTree<BehaviourClient>
as a subtree of a BehaviourTree<SimpleBehaviourClient>
). An optional string can be used as parameter when calling .tree()
to serve as the tree name. If specified, it will be used in the behaviour visualizer window as well as in the logged stack trace in case an exception happens during this subtree execution.
Leaf are the actions that will be executed by the client. They can be added to a Builder using the .leaf()
function.
When building a leaf in a builder, an optional string can be used to name the node. If specified, it will be used in the behaviour visualizer window as well as in the logged stack trace in case an exception happens during this leaf execution. The following trees result in the same behaviour being executed, but the second one will be easier to debug thanks to the named leaf. You can mix named and anonymous leaves in a same tree builder.
auto tree_with_anonymous_leaf = Builder<SimpleBehaviourClient>()
.leaf(Say, "Hello!");
auto tree_with_named_leaf = Builder<SimpleBehaviourClient>()
.leaf("My Awesome Name", Say, "Hello!");
A decorator is a special type of nodes that has only one child, and changes its result. Botcraft has some prebuilt decorators such as the succeeder (that always returns Success, independantly of its child result) or the inverter (that converts a Success in a Failure and a Failure in a Success). To add a decorator in a tree, one can either use the short version for prebuilt ones, or the full type for custom ones. In both cases, an optional string can be used to name the node. If specified, it will be used in the behaviour visualizer window as well as in the logged stack trace in case an exception happens during this decorator execution.
Custom decorator can also be defined, by inheriting the Botcraft::Decorator
class. When doing so, you must either add using Decorator<T>::Decorator;
to your class to have access to base Decorator constructor, or have all your custom class constructors taking a std::string (or better a const std::string&) as first parameter.
auto tree = Builder<SimpleBehaviourClient>
.sequence()
.inverter() // <-- shortcut to an anonymous predefined decorator
.leaf(Foo)
.decorator<RepeatUntilSuccess<SimpleBehaviourClient>>(5) // <-- example of an anonymous custom decorator with parameter
.leaf(Foo)
// Same thing but with named decorators
.inverter("Named inverter").leaf(Foo)
.decorator<RepeatUntilSuccess<SimpleBehaviourClient>>("Named RepeatUntilSuccess with parameter", 5).leaf(Foo)
.end();
A composite is a special type of nodes that has multiple child, and aggregates their results to get its final result. There are two mains composite, both implemented in botcraft: sequence and selector. A sequence is basically a logical AND between all the children: they are ticked one after the other, returning Failure as soon as one of them fails, or success if they all succeed. A selector is a logical OR: all the children are ticked one after the other, returnin Success as soon as one of them succeeds, or Failure if they all fail. To add a composite in a tree, one can either use the short version for prebuilt ones, or the full type for custom ones. Their declaration has to be closed by a call to .end()
. When creating a composite in a builder, an optional string can be used to name the node. If specified, it will be used in the behaviour visualizer window as well as in the logged stack trace in case an exception happens during this composite execution.
Like for the Decorator, custom composite can be defined by inheriting the Botcraft::Composite
class. When doing so, you must either add using Composite<T>::Composite;
to your class to have access to base Composite constructor, or have all your custom class constructors taking a std::string (or better a const std::string&) as first parameter.
auto tree = Builder<SimpleBehaviourClient>
.sequence() // <-- shortcut to a prebuilt composite
.leaf(Foo)
.leaf(Foo)
.leaf(Foo)
.composite<Selector<SimpleBehaviourClient>>() // <-- example of a full composite creation
.leaf(Foo)
.leaf(Foo)
.leaf(Foo)
.end()
.sequence("Named sequence") // <-- named sequence
.leaf(Foo)
.end()
.composite<Selector<SimpleBehaviourClient>>("Named Selector") // <-- named selector
.leaf(Foo)
.end()
.end();
Behaviour trees can be used even with custom client classes, as long as they are derived from TemplatedBehaviourClient
. Here is an example of such a client, with an action using the new field and a tree defined with this action. Note that very often, the same thing could be achieved with a SimpleBehaviourClient
and the blackboard.
If you're using the behaviour trees totally outside of Botcraft system, you can even use any type you want as context. A BehaviourTree or BehaviourTree are valid and will compile and behave as expected when ticked.
class MyClient : public TemplatedBehaviourClient<MyClient>
{
public:
int foo;
}
Status MyAction(MyClient& client)
{
std::cout << client.foo << std::endl;
return Status::Success;
}
auto tree = Builder<MyClient>()
.leaf(MyAction);