Skip to content

How to create a BehaviorTree

In this tutorial series, most of the time Actions will just print some information on console, but keep in mind that real "production" code would probably do something more complicated.

The source code of this tutorial is example/tutorial_1.py.

How to create your own Action

Firt, you have to wrote your function (async or sync) as normal, like this:

def approach_object(name: str):
    print(f"approach_object: {name}")

def check_battery():
    print("battery ok")

async def say_hello(name: str):
    print(f"Hello: {name}")

At this point, this is not (yet) a behavior action. To define an action, you have to use action function:

import async_btree as bt

approach_house_object_action = bt.action(target=approach_object, name="house")

check_battery_action = bt.action(target=check_battery)

say_hello_john = bt.action(target=say_hello, name="John")

With a class like this one:

class GripperInterface:

    def __init__():
        self._open = False


    def open(self):
        print("GripperInterface Open")
        self._open = True

    def close(self):
        print("GripperInterface Close")
        self._open = False

We can define action for these functions: - GripperInterface.open - GripperInterface.close

Create a tree dynamically

We will build a sequence of actions like this one: - say hello - check battery - open gripper - approach object - close gripper

To do that, we need to use sequence methods.


gripper = GripperInterface()

b_tree = bt.sequence(children= [
    bt.action(target=say_hello, name="John"),
    bt.action(target=check_battery),
    bt.action(target=gripper.open),
    bt.action(target=approach_object, name="house"),
    bt.action(target=gripper.close)
])

Run it:

with bt.BTreeRunner() as runner:
    runner.run(b_tree)

And you should see:

Hello: John

Why we did not see other action ? It's because our first action did not return a success (something truthy). So we could add a return True, on each our function, like this:

def approach_object(name: str):
    print(f"approach_object: {name}")
    return True

Or we could rewrote our behavior tree with specific status:

b_tree = bt.sequence(children= [
    bt.always_success(child=bt.action(target=say_hello, name="John")),
    bt.always_success(child=bt.action(target=check_battery)),
    bt.always_success(child=bt.action(target=gripper.open)),
    bt.always_success(child=bt.action(target=approach_object, name="house")),
    bt.always_success(child=bt.action(target=gripper.close))
])

If we running it again:

Hello: John
battery ok
GripperInterface Open
approach_object: house
GripperInterface Close

As you could see: - we use a single instance of GripperInterface - we have hard coded name on our action function

We can also define a function like this:

def check_again_battery():
    print("battery dbl check")
    # you should return a success
    return bt.SUCCESS

and wrote our behavior tree :

b_tree = bt.sequence(
    children=[
        bt.always_success(child=bt.action(target=say_hello, name="John")),
        bt.action(target=check_battery),
        check_again_battery,  # this will be encapsulated at runtime
        bt.always_success(child=bt.action(target=gripper.open)),
        bt.always_success(child=bt.action(target=approach_object, name="house")),
        bt.always_success(child=bt.action(target=gripper.close)),
    ]
)

check_again_battery will be encapsulated at runtime.

If we running it again:

Hello: John
battery ok
battery dbl check
GripperInterface Open
approach_object: house
GripperInterface Close

In a real use case, we should find a way to avoid this: - wrote a factory function for a specific case - either by using ContextVar (from contextvars import ContextVar)

You could see a sample in this source is example/tutorial_2_decisions.py.