View Source Taido (Taido v0.1.0)
Taido
is a library for building and executing behavior trees in Elixir.
First, build your tree using functions from the Taido.Node
module.
Here's an example of a behavior tree for a spaceship undocking from a
space station and traveling to another waypoint.
alias Taido.Node
# At its highest level, we have two steps: undock, and navigate.
# A sequence executes each of its children in order until
# one fails.
Node.sequence([
# We want to undock, but it is possible that we are already undocked.
# A `select` node, also known as a fallback, executes its children
# in order until one of them succeeds.
Node.select([
# If you want to make your behavior tree more modular, decorator
# nodes like `invert` allow you to flip the result of another node.
# Here, we will use it to flip a `docked?` condition, so that
# this step will succeed if we are undocked.
# If it succeeds, this node's parent will not execute the subsequent
# children, because it is a select node.
Node.invert(
# A condition node takes a boolean function, and succeeds if it
# returns `true`, else it fails.
# This node will succeed if the ship is docked.
Node.condition(fn state ->
state.ship.docked?
end)
),
# Finally, we do something to affect the world. The `action` node
# gives you the tree's state object, and lets you modify it, then
# lets you return a success or failure status of your own.
# This will actually undock our ship, but only if the preceding
# child didn't succeed, because they're both inside a `select` node.
Node.action(fn state ->
{:ok, ship} = Spaceships.undock(state.ship)
{:success, Map.put(state, :ship, ship)}
end)
# Notice that our `select` node ends here.
]),
# The goal of this entire tree, we navigate. Since this node is
# a child of a `sequence` node, it gets executed only if its preceding
# sibling nodes all succeed.
Node.action(fn state ->
{:ok, ship} = Spaceships.navigate(state.ship, "MARS")
{:success, Map.put(state, :ship, ship)}
end)
])
Then you can execute the tree with Taido.tick/2
.
This will evaluate every node in order until either the tree was fully
evaluated, or until one of the nodes returned a :running
status.
state = %{ship: Spaceships.new()}
{status, updated_tree, updated_state} = Taido.tick(tree, state)
Two important concepts in Taido
are state and status.
- Taido treats state similarly to a
GenServer
's state. Every time a behavior tree is evaluated, you must provide astate
variable. This can be whatever data type you want; Taido only passes it into each node in the tree so that you can update it, and returns the updated state to you. - A node returns a status of either
:success
,:failure
, or:running
. They can mean the same thing as Elixir's:ok
or:error
tuples, however they are mainly just feedback that a node gives to its parent, and eventually to you.
See Taido.Node
for the nodes provided by this library.
You can also create your own node by implementing the Taido.BehaviorTree
protocol.
Taido's behavior tree nodes are structs, and the Taido.tick/2
function
is pure and runs in a single process, so the only side-effects are the ones
you bring with you.
Asynchronous nodes
Asynchronous nodes are available, like Taido.Node.async_action/1
.
That node runs your action in a Task
and immediately returns :running
,
which causes the behavior tree to stop evaluating, saving its place to
resume later. Every time you run Taido.tick/2
, the task is checked,
and if it is done, its result is fetched, and the tree continues evaluating
like normal.
Warning
If you are running a behavior tree inside of anything like a
GenServer
, which automatically await tasks, you must forward messages sent to theGenServer
toTaido.handle_message/2
. Otherwise, the tasks in your behavior tree will never be completed. It's as simple as this:def handle_info(msg, state) do Taido.handle_message(state.behavior_tree, msg) end
Summary
Functions
Handle a process message.
Terminates all asynchronous tasks in the behavior tree.
Evaluate the behavior tree.
Functions
Handle a process message.
You will only need to use this function if you are ticking asynchronous
nodes in a context that automatically awaits tasks, like inside
a GenServer
. In that case, you must forward the GenServer
's messages
to Taido
like this:
def handle_info(msg, state) do
Taido.handle_message(state.behavior_tree, msg)
end
Terminates all asynchronous tasks in the behavior tree.
Evaluate the behavior tree.
Returns a tuple of {status, updated_tree, your_state}
.
Make sure to save the updated_tree
somewhere, especially if you are using
asynchronous nodes, because you must keep track of which tasks are
currently waiting to be checked.