How do I... Conditionally run actions?
The power of the bots is that they are organic, and can respond to their current context. In order to work that way, they require logic operations to choose which action to take next. How can you encode logic in the bots?
The number one takeaway is to use the structure of the tree, rather than code within the actions, to get organic behavior. Be sure you fully understand how behavior trees work!
Remember that each node in your tree only has two "transitions," :succeed
or
:fail
. You can create complex transitions from one action to another with only
these two outcomes by the way you nest the different types of control nodes.
Here are some examples of common logical structures.
If/Then
If/then is as simple as using the sequence
control node. Each action will only
occur if the previous one succeeds.
def if_then_tree() do
Node.sequence([
attempt_xyz_tree(),
post_xyz_tree()
])
end
Keep in mind that if any of the actions :fail
, the whole sequence
node will
:fail
. The always_succeed
control node is useful if you want to attempt an
action but don't want its outcome to affect the sequence
it is a part of, like
this:
def if_then_tree() do
Node.sequence([
attempt_xyz_tree(),
Node.always_succeed(post_xyz_tree())
])
end
In this example, the if_then_tree
will :succeed
as long as attempt_xyz_tree
succeeds, regardless of the outcome of post_xyz_tree
.
If/Else
If/else is simply the select
control node. Each action will be attempted until one
succeeds. Keep in mind that if all of the actions :fail
, the whole select
node will :fail
.
def if_else_tree() do
Node.select([
attempt_xyz_tree(),
fallback_to_abc_tree()
])
end
If/Then/Else
You can build up more complex logic by combining the two approaches above.
def if_then_else_tree() do
Node.select([
Node.sequence([
attempt_xyz_tree(),
post_xyz_tree()
]),
fallback_to_abc_tree()
])
end
In this example, if attempt_xyz_tree
succeeds, post_xyz_tree
will run, and
(assuming that succeeds) if_then_else_tree
will succeed. If attempt_xyz_tree
fails, then fallback_to_abc_tree
will run (and if_then_else_tree
will fail or
succeed depending on that outcome).
Keeping trees organized
You might have noticed in the example above, that the first subtree in the top level
select
is the same
as if_then_tree
.
When nesting trees, it is much easier to keep track of the flow if you name each of your subtrees and refer to them by name.
Like this:
def if_then_else_tree() do
Node.select([
if_then_tree(),
fallback_to_abc_tree()
])
end
Final thoughts
These approaches will let you build up transitions that are as complex as you need. Using the structure of the tree to control the logic has two benefits. First, you can mix and match the same actions in very different behaviors without having to change any code in the actions. Second, you can make more complex behaviors simply by nesting existing trees.
You may find it helpful to create a few actions that perform a simple check about the bot's current state to use as the "pivot" in your logic branches. For example, an action that checks if the bot is logged in:
def is_logged_in?(%{token: token}) when is_binary(token), do: :succeed
def is_logged_in?(_), do: :fail
Keep in mind that the always_succeed
, always_fail
and negate
control nodes can
come in handy. And lastly, avoid unnecessary nesting, such as having a sequence
inside another sequence
, which wouldn't have any effect on the logic flow.