Behavior trees on actual useApr 13, 11:04 AM

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.

Back to basics

I’ve been getting some questions about proper use of Behavior Trees. My previous tutorials focused more on how Behave actually worked and what each component meant, instead of showing some more complex examples, so people have come up with questions like how do I integrate animations into the tree? or how do you react?.

What follows are a few examples of how we have done things, not necessarily the only or best way to do it. With that out of the way…

How do you know what you’re doing?

There are actions that may take place over more than one tick, for instance if your agent is patrolling an area. As you can see in our selectors tutorial, I keep track of which action was last executed with a currentAction variable.

This is fine for small project, but the code can and does get a little repetitive. We just centralize everything in a CheckCurrentAction method, which

  1. Returns false if the last action was different from the current one.
  2. If necessary, changes the current action to the one received
  3. If we’re changing actions, track the time at which the change occurred.

csharp:
  1. // Returns true if the last action equals the one being sent. It also
  2. // configures the object according to the action parameters stored
  3. // for the current action
  4. protected bool CheckCurrentAction(BLSample.Actions action)
  5. {
  6.     // Compare against currentAction
  7. }

We can then easily check if this is the first time we get called or if it’s a recurrent call:

csharp:
  1. BehaveResult InterceptEnemies()
  2. {
  3.     if (!CheckCurrentAction(BLBallKickerLibrary.Actions.InterceptEnemies))
  4.     {
  5.         // Do whatever we have to do only the first time we get called
  6.     }
  7.     //
  8.     // Normal action code executed every time
  9.     //
  10.     return BehaveResult.Running;
  11. }

AngryAnt plans to add support on a future version for the last executed action on Behave itself, so some of this code will no longer be necessary.

Playing animations

Some actions can be associated with animations – for instance, if your agent just started patrolling an area, you want him to play a walking animation as a cycle; or if he just located an enemy, you want him to play once the action of aiming at him.

We could have specific code for this on every action, but since with CheckCurrentAction we already have a function that we call to check if an action has just started executing, why not centralize animation playing there?

What we do is keep a dictionary of action details, indexed by the Behave action identifier, that is static to our agent’s class.

private static Dictionary<BLSample.Actions, ActionDetail> actionDetails;

The action details can be any values that you need to set up on initialization, for instance the animation to play and how to go about playing it:

csharp:
  1. public struct ActionDetail {
  2.     // Current action
  3.     public BLSample.Actions Action;
  4.     // How long before we bail out
  5.     public float ActionDuration;
  6.  
  7.     // What animation we should start playing, if any
  8.     public string DefaultAnimation;
  9.     // Should the animation be queued?
  10.     public bool   Queue;
  11.     // Are we playing it once or looping it?
  12.     public WrapMode WrapMode;
  13.  
  14.     // Any other values and constructors go here
  15. }

This dictionary gets initialized once, and we then consult it on CheckCurrentAction.


protected bool CheckCurrentAction(BLSample.Actions action)
{
    //
    // Perform current action comparison
    //
    if (!theSameAction && actionDetails.ContainsKey(action))
        ActionDetail ad = actionDetails[action];
        //
        // Play animation and perform agent setup
        //
    }
    return theSameAction;
}

We can also expand it to override if animations should be played at all if necessary.

Expiration date

Take a look at the Behavior tree for our soccer player example:

BT3 01 Tree 400x244.Shkl

If you see the first three actions from left to right, they are

  • LocateBall
  • IsCloserToBall
  • AcquireBall

The first two are clearly atomic actions, whereas AcquireBall is clearly a task that will likely take a bit of on-going work, since we need to move to where the ball is before we acquire it. In theory we want to continue trying to acquire the ball until we succeed or fail, but in some cases we may want to have the action time out after some time, to give the rest of the tree a shot.

There are two ways if doing this:

Return Success instead of Running

We could take the simpler path and simply return BehaveResult.Success so that the tree starts at the beginning every time. This means that we would need to take care not to repeat time-consuming actions – for example, if we are following a path, we don’t want to re-evaluate the path every time.

Which stinks.

This approach is seriously kludgy, can result in unnecessary evaluations, and worst of all, decouples your tree’s logical organization from what is actually being executed. It’s not something I recommend.

Timing out

It’s much better to allow for some action expiration time, after which the tree is re-evaluated. If you paid close attention to the struct declaration above you saw an ActionDuration. This is a value in seconds after which the current action is considered expired. Since on CheckCurrentAction we initialized the time at which we began executing, we should also save the allowed duration for the current action, and we can then evaluate them easily.


protected bool HasActionExpired()
{
    bool expired = currentActionTime + actionDuration < Time.time;
    return expired;
}    

Our AcquireBall action would then be extended to handle this expiration time:

csharp:
  1. BehaveResult AcquireBall()
  2. {
  3.     if (!CheckCurrentAction(BLBallKickerLibrary.Actions.AcquireBall))
  4.     {
  5.         // Do whatever we have to do only the first time we get called
  6.     }
  7.     if (HasActionExpired())
  8.         return BehaveResult.Failure;
  9.     //
  10.     // Normal action code executed every time
  11.     //
  12.     return BehaveResult.Running;
  13. }

Shifting our focus

Say you have a little demon agent, happily returning BehaveResult.Running to his AcquireMaiden action as he pursues a hapless girl through the countryside. Then out of the blue an OUCH! event gets raised as he is struck by an arrow from a hunter he hadn’t noticed.

Now, our friendly fiend probably has an IsBeingAttacked somewhere in his tree for exactly this sort of situation. Unfortunately, he won’t get to evaluate it, since he’s not too smart and we forgot to bail out of the chase to handle the hunter.

We should react to the hunter’s arrows to avoid having him turn into a devilish pincushion, so what we do is set a flag whenever we are attacked that the current action should be interrupted.

Here’s one way.

csharp:
  1. void _Attacked(MessageAttacked message)
  2. {
  3.     // Do something dammit!
  4.     InterruptAction = true;
  5. }

InterruptAction is a property flag, which gets cleared every time we check it:

csharp:
  1. protected bool InterruptAction
  2. {
  3.     set
  4.     {
  5.         interruptAction = value;
  6.     }
  7.     get
  8.     {
  9.         bool stop = interruptAction;
  10.         interruptAction = false;
  11.         return stop;
  12.     }
  13. }

We should then check this property in any action we want interupted, so that we can re-evaluate the tree instead of continuing along like nothing’s happening:

csharp:
  1. BehaveResult AcquireMaiden()
  2. {
  3.     if (!CheckCurrentAction(BLSample.Actions.AcquireMaiden))
  4.     {
  5.         // Do whatever we have to do only the first time we get called
  6.     }
  7.     if (HasActionExpired())
  8.         return BehaveResult.Failure;
  9.     if (InterruptAction)
  10.         return BehaveResult.Success;
  11.     //
  12.     // Normal action code executed every time
  13.     //
  14.     return BehaveResult.Running;
  15. }

Notice that in this case we return Success instead of Failure, since we don’t want to move to the next action on the tree but instead to have the whole tree re-evaluted, in case something needs to take higher priority (like the whole not dying thing).

BehaveResult.Running

That should cover the basics of how to get behavior trees integrated into your game flow. I should note that as AngryAnt has pointed out, both the Time out and the change in focus could be handled in a Decorator as well, but we haven’t described those yet, so I’ll go into them in the next tutorial.

If you have any outstanding questions, drop me a note on the comments and I’ll cover those topics in a future article.

by Ricardo J. Mendez

Commenting is closed for this article.