Why Behavior Trees?
Simulating real users is hard. When testing, we want to be able to describe how to react to a given situation, without being too rigid. We'd like to be able to describe complex behaviors and conditional sequences in a way that is easy to manage and change.
Behavior trees are the technology that power the non-player-characters' AI in video games. They are similar to finite state machines, but simpler to manage, especially as they get more and more complex.
The secret to their simplicity and power come from 5 fundamentals:
- The data structure is a tree. This makes it composable, meaning you can build up complex behaviors by nesting smaller subtrees.
- Each node has 2 possible transitions: "fail" or "succeed." These are the only 2 transitions, no matter how many nodes you add.
- Leaf nodes are actions. Actions can do what ever you want, including reading the bot's state, performing side effects, and updating the bot's state. Actions must report if they failed or succeeded.
- Internal nodes are control nodes - they control traversal through the tree. There are a number of available control nodes. The most common ones are "sequence" and "select." Sequence nodes will attempt to run each child from left to right. If each child succeeds, it will succeed. If a child fails, it will fail. Select nodes are the inverse - if all children fail, it fails, otherwise it succeeds when one of its children succeeds.
- One leaf node is always "active." When it fails or succeeds, the tree will traverse to the next leaf node, based on the structure of the tree.
The power of behavior trees comes from how you nest control nodes.
Each bot has a behavior tree "template" to follow. It run in a loop where it runs the current action, gets the outcome, traverses to the next action, and repeats. In the Bot Army, trees and actions are kept separate for organization, but also because actions can be reused in different orders in different trees.
A behavior tree looks like this:
def tree do
sequence([
action(MyActions, :get_ready),
action(CommonActions, :wait, [5]),
select([
action(MyActions, :try_something, [42]),
action(MyActions, :try_something_else),
action(CommonActions, :error, ["Darn, didn't work!"])
]),
MyOtherTree.tree(),
action(CommonActions, :done)
])
end
The bot would call these corresponding actions:
def get_ready(context) do
{id: id} = set_up()
{:succeed, id: id} # adds `id` to the context for future actions to use
end
def try_something(context, magic_number) do
case do_it(context.id, magic_number) do
{:ok, _} -> :succeed
{:error, _} -> :fail
end
end
def try_something_else(context), do: ...
Read more about behavior trees, or watch a video.