I recently went through 3ish years of a large robotics project where the main focus was on doing complex robotic tasks through behavior trees. There were a couple of limitations on the trees:
- They had to be binary.
- They had to return either true or false
- They could not pass information at runtime between each other as arguments.
- We had the following conditional operators: Sequence (&&), Select (||), Parallel (*), While, For, and Match (which is a sort of switch).
Each behavior was a C++ class with a single function called "execute" which returned true or false. This function was then called in a thread. To develop behavior trees, we used a horrible combination of C++ operator overloading, macros, and so on. We could then view the resulting behavior tree in a little window with colorful boxes and arrows. The resulting code ended up looking something like this:
// A very high level robotics task. Robot initializes, searches for a rock, and tries to pick it up
// with either its right or left hand.
Behavior& PickUpRock()
{
return
// These are examples of behavior creation macros which take in an ordinary C++ function. They get expanded
// into something along the lines of Behavior(name, boost::bind(function, argument 1, argument 2, ...))
BEHAVIOR(InitializeSystem)
&& BEHAVIOR1(SearchFor,"table")
&& BEHAVIOR1(SearchFor,"rock")
// Choosing left or right hand involves the usage of a cryptic switch
// statement substitute.
&& Match(IsOnLeft("rock"), 1,
Grasp(LEFT_ARM, "rock")
BEHAVIOR1(GoHome, LEFT_ARM)
)
|| Match(IsOnLeft("rock"), 0,
Grasp(RIGHT_ARM, "rock")
BEHAVIOR1(GoHome, RIGHT_ARM)
);
}
// This is an example of a sub-behavior which uses arguments
// It is just a sequence of less abstract sub-behaviors.
Behavior& Grasp(Arm arm, string object)
{
return CreateGraspSet(arm)
&& PlanToGrasp(arm)
&& GraspPreshape(arm, object)
&& ServoToTarget(arm)
&& CloseHand(arm);
}
// Many thousands of lines defining all the sub-behaviors follow in multiple files...
// At the very bottom we finally get to write straight C++ code. It will usually be just a dozen or so lines.
// If the Behaviors fail to compile you will get very cryptic error messages from boost and STL.
You can see that what's happened is we've just created a less useful, harder to write meta-language on top of C++, instead of using C++ directly. Even worse, we were forced to use static singletons everywhere just to transfer data between behaviors. This led to things being extremely hard to debug, since we couldn't easily infer what data was getting changed where by which behavior. Contrast this with simply writing the whole thing in an ad-hoc script, where you can store local data and very clearly see where the data is going to and where it comes from.
I've yet to find a BT implementation which doesn't involve either extreme verbosity or crypitc meta-language symbols.
Take for example this library, which in its main example, takes dozens upon dozens of cryptic lines full of "news" to make what is equivalent to a few function calls and a while loop within if/else brackets -- or perhaps this library, which uses cryptic C# operators to make a meta-language, much like in my example. I'm beginning to suspect that the whole drive behind BT is to make it easy to create plug-and-play graphical programming in an editor (why you would want to do this if you're not writing middleware for non-programmer designers I have no idea).
Is this how BTs are really used in the industry? Is there a better way?