Behavior trees – Branching paths with selectors
NOTE: These tutorials were created for Behave 0.3, and Behave has changed considerably since then. You can use them as guidelines for concepts, but do not expect the code will work straight out with the latest libraries.
Introduction
On our last tutorial we looked at how to do a sequence of actions, when one of these needs to be an on-going task. That example was a straightforward sequence: execute task A, if successful move on to B, etc., and restarting if any task fails. Let’s now look at how to implement branching behavior with selectors.
As before, we’re using AngryAnt’s Behave library for Unity. If this is your first encounter with behavior trees or Behave, you may want to read the following items first:
- AngryAnt’s original tutorial
- Behavior Trees in Unity with Behave
- Behavior Trees – Going for the ball
Each one build on the previous one, so it’s important that you have the concepts clear before proceeding.
Selectors in the tree
Let’s start by downloading the project files. As you can see it build upon our previous project, so the old BallKicker tree is still around, but now we have a new tree named TeamPlayer.

This tree not only is a lot more complex than what we have done right now, but it actually looks like a proper tree:
A bit more involved than this fellow, isn’t it? Now, how are you supposed to interpret it?
Behavior trees are executed hierarchically, from top to bottom, left to right. What matters is the order of the connectors, not how they are positioned on the canvas, so in this case the item tagged TeamPlayer would be evaluated first, then Choose Actions, then Go for the ball, and so on.
You’ll notice that the main item this time around (TeamPlayer) is one that we haven’t used up until now. It’s called a Selector, and it’s purpose is to execute actions not only in a sequence, but moving on to the next one if the current one fails. The selector itself returns successfully once one of its sub-actions has been successful, and fails if it reaches the end of the actions without any returning Success. This means that not all actions need to be executed, so you should arrange them in priority order.
So how would you read that tree, in plain English? For a Team Player:
- Try to perform one of a sequence of actions
- Attempt to perform at least one of the following actions
- Go for the ball
- Try to locate the ball
- If successful, figure out of we’re closer to the ball than other teammates
- If so, attempt to acquire the ball!
- Once we have it, either
- Pass the ball, which occurs
- If we’re overwhelmed
- And we can pass it to someone
- Or simply kick the ball forward
- Run forward, if any of the previous actions failed
- First attempt to locate a friend with the ball
- If we do see him, attempt to intercept a nearby enemy
- As a last resort, move towards other players
- First attempt to locate moving players
- If we do see some, move towards them
- If that failed, Patrol
Can you see the glorious noodliness of it all? Imagine attempting to implement this as a series of cases, the horror of a spaghetti mix of if-then-elses. Branching actions is one of the areas where behavior trees truly shine – as we’ll see, the code for implementing this mix of actions is no different from what we have done in our past examples: we simply implement each action, along with its possible result for the tree, and Behave takes care of the possible branching paths and behavior flow.
On to the behaviors
Let’s look at a few of the methods, starting with our TickAction implementation:
public BehaveResult TickAction( int action, Tree sender, Object data )
{
switch( ( BLBallKickerLibrary.Actions )action )
{
case BLBallKickerLibrary.Actions.LocateBall:
return LocateBall();
case BLBallKickerLibrary.Actions.IsCloserToBall:
return IsCloserToBall();
case BLBallKickerLibrary.Actions.AcquireBall:
return AcquireBall();
case BLBallKickerLibrary.Actions.KickBallForward:
return KickBallForward();
case BLBallKickerLibrary.Actions.IsOverwhelmed:
return IsOverwhelmed();
case BLBallKickerLibrary.Actions.PassTheBall:
return PassTheBall();
case BLBallKickerLibrary.Actions.LocateFriendWithBall:
return LocateFriendWithBall();
case BLBallKickerLibrary.Actions.InterceptEnemies:
return InterceptEnemies();
case BLBallKickerLibrary.Actions.Patrol:
return Patrol();
}
return BehaveResult.Failure;
}
As you can see, the method looks exactly like the one we had done in our previous example: there is no special code to deal with the selectors and the branching paths. Whenever an action needs to take place, Behave will call that method with the action identifier, like it does when executing actions in a sequence. Notice that like with sequences, we don’t have any case items for the selector itself, since Behave takes care of that – we only need to implement the leave nodes.
Since the purpose of this tutorial is to review Selectors, we won’t go into detail of each method, some of which are somewhat rudimentary, but will instead walk through a possible tree path.
Walking the tree
We will follow the path of a team player that sees the ball, attempts to go for it but is pre-empted by a friendly team player who gets there first, so our agent has to find an enemy agent to intercept.
Locating the ball
The first order of business is attempting to locate the ball. This is not an on-going task, we simply return Success if the variable canSeeBall is true, and Failure if it’s not.
BehaveResult LocateBall()
{
// Straighten it up
transform.rotation = Quaternion.identity;
BehaveResult result = canSeeBall ? BehaveResult.Success : BehaveResult.Failure;
return result;
}
Determining if the team player can see the ball or not is done via a trigger. For more information on that, see our tutorial on proximity detection with triggers.
We have two possible paths: if we can see the ball, Behave will then move us on to the action of evaluating if we’re closer to it. If not, it’ll break out of the current sequence and attempt the next one (Run Forward). Let’s suppose we can see the ball.
Are we close?
The next method on the sequence is also a straightforward one:
/*
Calculates if we’re the closest to the ball from the players we
can see.
*/
BehaveResult IsCloserToBall()
{
float dist = this.DistanceToBall;
// Go through each nearby friendly to check how close they are
foreach (DictionaryEntry de in nearbyFriendlies)
{
GameObject friendly = de.Value as GameObject;
float friendlyDist = GetController(friendly).DistanceToBall;
if (friendlyDist < dist && friendlyDist < ballOwnershipDistance)
return BehaveResult.Failure;
}
return BehaveResult.Success;
}
Easy enough – walk through each friendly player that we can see and evaluate if they’re both closer than use, as well as being close enough to the ball that we can consider them having ownership of it. Much like in the previous one, we simply return success or failure – this is a one-shot condition. Let’s assume the demo just began, and ownership of the ball is still contested. We’ll return BehaveResult.Success and Behave will move us on to the next task.
Go for it!
/* This action will return Success once we’re close enough to the ball for kicking,
and Running meanwhile. While the action is Runnning, our Update method will
keep moving us closer.
*/
BehaveResult AcquireBall()
{
SetCurrentAction(BLBallKickerLibrary.Actions.AcquireBall);
// By default we simply continue to run
BehaveResult result = BehaveResult.Running;
// Since WaitForBall evaluates that we do have a ball object assigned,
// we can safely assume that one exists if we got this far.
if (HasBall)
{
// Notify our visible friends that we have the ball
NotifyFriendsOfAcquisition();
SetCameraToFollow();
result = BehaveResult.Success;
SetCurrentAction(BLBallKickerLibrary.Actions.Unknown);
}
else if (Time.time – timeLastAction > pursuitSeconds || playerWithBall != null)
// Too much time has passed – fail
result = BehaveResult.Failure;
return result;
}
You’ll notice that the first thing we do is call SetCurrentAction. This is a method in our agent that is necessary if we want to keep track of which task we are on, since Behave doesn’t provide access to the tree’s currently running action. On it, we also mark the time that the action began executing.
// Tracks the current action, and sets the time at which it was changed
void SetCurrentAction(BLBallKickerLibrary.Actions action)
{
if (currentAction != action)
{
currentAction = action;
timeLastAction = Time.time;
}
}
Notice that it is important to ensure that this is the first time we’re setting this specific action, since an action may be called multiple times if it returns BehaveResult.Running, like AcquireBall is likely to do.
In this case, the action will continue to run until either:
- We have the ball,
- The time we alloted for ball pursuit has passed, or
- Someone else from our team has the ball
If we have the ball, we return BehaveResult.Success so that Behave can move us to the next action, otherwise we return Failure to indicate we have failed in acquiring the ball. In our example someone else from our team got to the ball first. That actor will then move on to IsOverwhelmed, to evaluate if they have to pass the ball, while we will be sent out of this branch and into the next item on the selector, so we can attempt to intercept enemies.
Running interception
OK, this concept is closer to football than soccer. The original implementation attempted to position itself for better ball passing, but I found the resulting interactions relatively boring. Instead, we’ll find a nearby enemy and attack it! I’m sure FIFA would disapprove, but what the hell.
The first step, though, is making sure that we can see a friendly player who has the ball.
BehaveResult LocateFriendWithBall()
{
BehaveResult result = BehaveResult.Failure;
if (playerWithBall != null)
{
// Make sure he still has it
if (GetController(playerWithBall).HasBall)
result = BehaveResult.Success;
else
playerWithBall = null;
}
return result;
}
This is important, since there are many paths by which we could have gotten here. For instance, we could have failed to find the ball altogether in the last branch, which would mean that we probably can’t find the player with the ball either. In that case, we would return BehaveResult.Failure, and Behave would take us to the next sequence on the Choose Actions selector. In our case we got here because someone else from the team got to the ball before our agent, so we will confirm that we can locate the friend owning the ball and Behave will move on to execute interception.
BehaveResult InterceptEnemies()
{
// Only acquire a target if we’re not already on this action
if (currentAction != BLBallKickerLibrary.Actions.InterceptEnemies)
{
// The actual running takes place on Update
bool hasTarget = AcquireTarget();
if (!hasTarget)
return BehaveResult.Failure;
}
SetCurrentAction(BLBallKickerLibrary.Actions.InterceptEnemies);
BehaveResult result = BehaveResult.Running;
if (HasBall || Time.time – timeLastAction > blockTargetSeconds)
{
// Once we’re done, can forget about our enemy target
enemyTarget = null;
result = BehaveResult.Success;
}
return result;
}
We first attempt to acquire a target, which is done by a random selection among a list of visible enemies, giving priority to the one closest to us. If we can get one, we then set the current action, and allow it to run for as long as the time we have allotted for blocking enemies. This takes place on several methods:
- First, on the Update method we will move closer to the target enemy, assuming we have one.
- Then, once we get there and collide with him, OnCollisionEnter will fire and we’ll evaluate a random chance of pushing the enemy away via random force.
If we can’t acquire a target because we don’t see any enemies nearby, we will return Failure and Behave will move on to the next case in the selector – attempting to move towards other players.
There is always the chance that we will acquire the ball during this process. If that happens, or the time we had allotted for blocking runs out, we return Success and the Run Forward case of the Selector will return Success as well. In this case, the Choose actions selector will return Successful, and won’t move on to the next case of the TeamPlayer selector. The tree will restart, giving us a chance to have another run through it and maybe acquire the ball this time.
All together now
On the following screenshots you can see the state of the actors through a run of the simulation.

The red cubes you see here are going for the ball, while the magenta ones are running interception against an enemy player. The lines (visible only in debug mode or with gizmos enabled) show the intercepting cubes’ targets.


Here we can see not only players and their targets, but one player’s range of visibility as defined by a sphere trigger.

Finally, you can see a live run of the simulation here.
Conclusion
As you could see through this article and its accompanying project, behavior trees allow us to focus on the particular logic for the individual actions we need to implement, while allowing us to implement even complex chains of actions in a straightforward manner. It shouldn’t be too difficult to notice that even if we decide that the flow through our behavior three needs to change, we likely won’t need to alter our code at all – we will simply update our tree’s structure, and allow Behave to take care of the rest.
What’s next?
I’d like to go into more complex cases, such as nesting trees inside trees, but those areas of Behave seem to still be a work in progress. For starters, sequences, running actions and selectors should carry you far enough.
By now you should have a good grasp of how to perform branching behavior with selectors. I’ve intentionally left both the branch of the tree dealing with moving towards other players if we can’t see the ball, as well as the Patrol action. Why not implement them yourself and get some hands-on experience with Behave?










