• Home
  • About us
  • Articles and Tips
  • Contact us

Articles

Home / Articles / Behavior trees – Going for the ball

Behavior trees – Going for the ball

Posted on: 12-8-2008 Posted in: AI, Unity

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 first behavior tree tutorial, we explored how the actions in a behavior tree are sequenced with a very basic example. The code we did was a very simple example of how a sequence moves from one action to the other, if the first one succeeds, but the application hardly did anything useful. Let’s take a look now at how to deal with a sequence of actions where at least one of them needs to be an on-going task.

On this tutorial we won’t go over each step for creating the application, since we already explored that. Instead we will review an application I’ve previously created, and dissect how each part works. You can first see the running example of several cubes playing with a ball here. Now, how does it work, and what do the colors mean?

You can get the project from here. Download it and open it in Unity, it’ll simplify following along with the description. As before, we are using AngryAnt’s Behave tool for Unity. Remember you’ll need to have the Mono compiler installed if you wish to rebuild the behavior tree library.

Implementation

Our project consists of two basic types of objects: a bouncy ball and a player who will kick it, represented in this case in glorious programmer art respectively as a sphere and a cube.

BH2 01 Player And Ball 500x314.Shkl

The ball will be ruled by physics, but we will create some simple rules for our kickers:

  1. If we don’t see a ball close to us, wait until we see it.
  2. Once we have seen the ball, start moving until we reach it.
  3. Kick the ball!
  4. Kicking tired us, so rest for a few seconds.

We’ll add the code for these actions in our BallKicker script.

Detecting the ball

The first step should be familiar – we saw how to do that when we performed proximity detection with triggers on our boids application. In this case, you’ll see that we have added a sphere trigger collider to our cubical kickers, and we handle the ball coming in and out of the area of detection via events.


public void OnTriggerEnter(Collider other)
{
// The trigger is used for ball proximity detection
if (other.gameObject == ball)
{
canSeeBall = true;
}
}

public void OnTriggerExit(Collider other)
{
// The trigger is used for ball proximity detection. If this fires,
// the ball has moved outside our detection area
if (other.gameObject == ball)
{
canSeeBall = false;
}
}

We’ve gone over the reasons why I like doing things this way in the previous article. Let’s now see about the behavior tree.

Creating the tree

You’ll see that I have a BallKickerLibrary, which contains one collection and one tree.

BH2 02 Collection

If you review the BallChaser tree, you’ll see that it contains one sequence with the four actions, in the corresponding order.

BH2 03 Chaser Tree 500x319.Shkl

We handle these events on our TickAction method, which simply calls a method for each separate case, and fail if we don’t recognize the action.


public BehaveResult TickAction( int action, Tree sender, Object data )
{
switch( ( BLBallKickerLibrary.Actions )action )
{
case BLBallKickerLibrary.Actions.WaitForBall:
return WaitForBall();
case BLBallKickerLibrary.Actions.GoToBall:
return GoToBall();
case BLBallKickerLibrary.Actions.KickBall:
return KickBall();
case BLBallKickerLibrary.Actions.RestAfterKick:
return Rest();
}

return BehaveResult.Failure;
}

On-going tasks

Up until now we have seen two possible BehaveResult values: BehaveResult.Failure, which means that the current action has failed and the sequence should be aborted; and BehaveResult.Success, which tells Behave to carry on with the next action. While this is enough for KickBall, clearly methods such as GoToBall and Rest have an ongoing component to them: in the first case, the kicker shouldn’t consider that action done until it’s reached the ball, and in the second until the rest time has passed. We can’t simply return BehaveResult.Failure, because then whole sequence would be executed again. So what do we do?

When we want to return from a call to TickAction but indicating that the action has neither completed successfully nor being considered a failure, we return BehaveResult.Running. This tells Behave: “don’t move on yet, give me a bit more time to do my thing” – and it’s precisely what we need for most of the actions in our kicker.

What we will do is handle each task independently, tracking what we’re currently executing, and on the call to Update modify our kicker’s status, whether it is by changing its position or indicating what we’re doing with a specific color.

On to the action handlers.

Waiting for the ball

Our WaitForBall method is very simple, made long only by our comments. Having handled ball proximity on the event triggers, we need only to wait until the canSeeBall variable becomes true. Notice how we’re tracking the currentAction, so that the Update method knows what to do.


BehaveResult WaitForBall()
{
BehaveResult result = BehaveResult.Failure;
if (ball == null)
{
Debug.Log(“A ball should have been assigned”);
}
else if (canSeeBall)
{
// We were waiting for the ball to enter our viewing area so we
// can go in and kick it. Move on to the next action!
result = BehaveResult.Success;
}
else
{
/*
We don’t have a ball nearby, so just wait until one lands
close to us.

BehaveResult.Running indicates to the library that the current
action is still executing.
*/
currentAction = BLBallKickerLibrary.Actions.WaitForBall;
result = BehaveResult.Running;
}
return result;
}

Simple, isn’t it? There are two ways in which we could have handled this action, since it’s the first one we execute: we could have done it this way, by telling Behave that the action of waiting for the ball is still running, or we could have called it LookForBall and return BehaveResult.Failure if we don’t see one. Since it’s the first action in the sequence, it’ll simply get called again. I prefer this method since not only it seems more natural, but it allows us to explore the concept of a running action.

Here you can see a selected kicker, its visibility trigger and the ball (if you squint).

BH2 04 Visibility

Going for the ball

Our GoToBall method is as straightforward as the previous one – we simply return that the action is still running until we have hit the ball.


/* 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 GoToBall()
{
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 (hitBall)
{
// We have collided with the ball – stop moving
hitBall = false;
result = BehaveResult.Success;
currentAction = BLBallKickerLibrary.Actions.Unknown;
}
else
// Tell Update we’re still executing
currentAction = BLBallKickerLibrary.Actions.GoToBall;
return result;
}

Now, how do we know that we have hit the ball? Colliders, of course! No need to go over the handlers here, since they’re trivial, but take a look at OnCollisionEnter and OnCollisionStay in the source.

Once this action returns BehaveResult.Success, Unity will tell our action tree that we’re ready to kick the ball.

Kicking

We kick the ball by applying a random force to it. We’ll take a few other actions when we do so, like altering the object speed and keeping track of the last time we took action (for resting purposes). Unlike the other three actions, this one returns BehaveResult.Success immediately because there is nothing left to do – we have already kicked the ball.


BehaveResult KickBall()
{
// Apply a random force to the ball. We’re a horrible kicker.
Vector3 v = Random.insideUnitSphere;
v.y = Mathf.Abs(v.y);
v *= 1000;

Vector3 forcePos = Random.insideUnitSphere;
ball.rigidbody.AddForceAtPosition(v, forcePos);

// Every time we kick the ball, our speedFactor changes again, to mix
// things up a bit
speedFactor = Random.Range(0.2f, 2.0f);
timeLastAction = Time.time;

// Once the force has been applied, return successfully. This is
// a one-shot action.
return BehaveResult.Success;
}

Resting

Having the object rest serves two purposes: first, we give other kickers a chance to get to the ball; and second, it allows us to demonstrate a state change in the game for didactic purposes. Since we need to wait for a certain number of seconds, we return BehaveResult.Running until the time has passed, at which point the sequence can begin again.


// Each kicker had to rest a certain number of seconds after hitting the ball
BehaveResult Rest()
{
if (Time.time – timeLastAction < restSeconds)
{
currentAction = BLBallKickerLibrary.Actions.RestAfterKick;
return BehaveResult.Running;
}
else
{
currentAction = BLBallKickerLibrary.Actions.Unknown;
return BehaveResult.Success;
}
}

Behaviors, all together now

The only thing we have left to do is act upon the currentAction value, which we do on the Update method. There’s no need to copy it here, since it’ll be easy to read for anyone familiar with Unity. What it does is assign a color to each state in the action tree, to represent it visually, and in the case where we’re going for the ball it moves the kicker closer.

So what our kickers do is:

  • Remain green until they have seen a ball
  • Turn red when they’re aggro and chasing the ball
  • Turn blue when they’ve kicked it and are resting

Notice that KickBall doesn’t have an associated case on Update, since its action takes place immediately. It doesn’t have an assigned color either, since it flickers away too quickly, but it could be extended to (for example) emit some dust particles when the ball gets kicked, or when we collide with it.

As you could see the example application has several kickers, scattered through the scene. We start with only three of them close to the ball (the camera will be following one of them), which will begin fighting over the ball as soon as the scene begins. Their cavorting may bring the ball close to one of the green kickers, at which point they’ll turn red and join the fray.

Conclusion

As you can see, implementing our agent flow with behavior trees helps us separate our logic in simple, discrete methods, and by taking care of action flow control leaves our code with only the methods relevant to its functionality. This will become more evident when we go into selectors and decorators in a future tutorial.

Meanwhile, here are some suggestions of modifications you can do to extend this example:

  1. Create a shy player, which only goes for the ball if there are fewer than 3 other kickers chasing it.
  2. Create an aggressive player, which kicks other players away if they get between him and the ball.
  3. Create two type of players, which will fight each other for control.

Until the next time!

About the Author

Ricardo J. Méndez

  • Popular Posts
  • Related Posts
  • Quick SteerToFollow example
    Quick SteerToFollow example
  • Screenshot Saturday 20110813
    Screenshot Saturday 20110813
  • Screenshot Saturday 20110806
    Screenshot Saturday 20110806
  • UnitySteer - AutonomousVehicles and Bipeds
    UnitySteer - AutonomousVehicles and Bipeds
  • Hairy Tales Previews
    Hairy Tales Previews
  • Quick SteerToFollow example
    Quick SteerToFollow example
  • UnitySteer - AutonomousVehicles and Bipeds
    UnitySteer - AutonomousVehicles and Bipeds
  • Hairy Tales progress - new elements and a boss
    Hairy Tales progress - new elements and a boss

Leave a Reply

Click here to cancel reply.

Recent posts

  • Hairy Tales Previews
  • Quick SteerToFollow example
  • UnitySteer – AutonomousVehicles and Bipeds
  • Hairy Tales progress – new elements and a boss
  • Public Alpha 2

Recent comments

  • Ricardo J. Méndez on Quick SteerToFollow example
  • Ben Ezard on Quick SteerToFollow example
  • Ricardo J. Méndez on UnitySteer 2.1 released
  • Elie on UnitySteer 2.1 released
  • Ricardo J. Méndez on Upcoming changes to UnitySteer
© 2009-11 Arges Systems Inc.. All Rights Reserved
TwitterStumbleUponRedditDiggdel.icio.usFacebookLinkedIn