Sunday, January 20, 2013

Behavior Tree

I have been experimenting with behavior trees for a bit now, always finding I am over complicating things or that I did not prepare to solve all my needs with them, but the potential exists.

Right now I am working on a flash game along side an artist (I am so glad I don't have to draw) and I implemented an straight forward almost component like behavior system, such as "I give you the draw behavior, therefore you draw".

This was great to start, I didn't put too much thought into it and I ended creating some pretty cool behaviors, like interpolations (much like the tween class in Unity, but not as huge, of course), recycling routines, click events, functions proxy (Delegate like things, but in As3), collision responses, sequences, etc.

The problem was there was no stop at some behavior, once the entity got the behavior there was no way to stop the behavior from happening, since there was no real condition evaluation unless I went out of my way and filled the behavior with possibilities within its routine (which is dirty).

So I created 2 supporting templates that would work together to make my behaviors only fire when the conditions apply.

First, I created a "BaseCondition":


public class BaseCondition implements IBehavior
 {
  static var Head:BaseCondition;
  var Next:BaseCondition;
  var Self:BaseCondition;


The first part explains that this class is the type of IBeahvior (To comply with my Entity IBehavior collection, and to not disturb what was already in place and working properly).

The Head, Next and Self are the elements used to keep this class pooled, if you would like to know more about it please check my previous post here

The IBehavior interface contains 2 functions, Update():void and Recycle():void, pretty simple.

var Condition:ICondition;
  
  public var TrueConditions:Vector.<BaseCondition>
  public var FalseConditions:Vector.<BaseCondition>
  
  public var TrueActions:Vector.<IBehavior>
  public var FalseActions:Vector.<IBehavior>
  
  public function BaseCondition()
  {
   Self = this;
   TrueConditions = new Vector.<BaseCondition>();
   FalseConditions = new Vector.<BaseCondition>();
   TrueActions = new Vector.<IBehavior>();
   FalseActions = new Vector.<IBehavior>();
  }


Then we have a field for the condition, this condition is what makes the base condition unique, depending on this ICondition we will be able to tell what we are looking for, such as "Is this element inside the view frustum?".

Then we have 4 collections, one for the true branch (in case the condition is true, follow up with these conditions), another for the false conditions and 2 more of the true and false actions, meaning that depending on the residing condition in this base condition we will execute either the actions in the true collection or in the false collection, this proves to be very useful as you start evolving your behaviors.

Coming next we have the factory patterns I use, they are essentially the same with one another except for a small difference, this is done for convenience when creating behaviors (to avoid a mess of having to create things, link them and then reuse them).


public static function Create(Condition:ICondition):BaseCondition
  {
   var Vessel:BaseCondition;
   if (Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new BaseCondition();
   }
   
   Vessel.Condition = Condition;
   
   return Vessel;
  }
  
  public static function CreateRoot(Owner:Entity, Condition:ICondition):BaseCondition
  {
   var Vessel:BaseCondition;
   if (Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new BaseCondition();
   }
   
   Vessel.Condition = Condition;
   Owner.BehaviorList.push(Vessel);
   
   return Vessel;
  }
  
  public static function CreateTrueBranch(PreviousBranch:BaseCondition, Condition:ICondition)
  {
   var Vessel:BaseCondition;
   if (Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new BaseCondition();
   }
   
   Vessel.Condition = Condition;
   PreviousBranch.TrueConditions.push(Vessel);
   
   return Vessel;
  }
  
  public static function CreateFalseBranch(PreviousBranch:BaseCondition, Condition:ICondition)
  {
   var Vessel:BaseCondition;
   if (Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new BaseCondition();
   }
   
   Vessel.Condition = Condition;
   PreviousBranch.FalseConditions.push(Vessel);
   
   return Vessel;
  }


There's nothing really special to tell, if the factory pattern is not really clear to you please read my last post, there I explain my pool system and the factory pattern used in it.

public function Update():void
  {
   if (Condition.CheckCondition())
   {
    var count:int = TrueActions.length;
    for (var i:int = 0; i < count; i++)
    {
     TrueActions[i].Update();
    }
    
    count = TrueConditions.length;
    for (var i:int = 0; i < count; i++)
    {
     TrueConditions[i].Update();
    }
   }
   else
   {
    var count:int = FalseActions.length;
    for (var i:int = 0; i < count; i++)
    {
     FalseActions[i].Update();
    }
    
    count = FalseConditions.length;
    for (var i:int = 0; i < count; i++)
    {
     FalseConditions[i].Update();
    }
   }
  }

And here we have the heart of this behavior tree, where we check the condition and depending on the outcome we will either do the true reaction or false reaction, and follow the proper path in the behavior.
public function Recycle():void
  {
   Next = Head;
   Head = Self;
   
   var count:int = TrueActions.length;
   for (var i:int = 0; i < count; i++)
   {
    TrueActions[i].Recycle();
   }
   
   count = TrueConditions.length;
   for (var i:int = 0; i < count; i++)
   {
    TrueConditions[i].Recycle();
   }
   
   count = FalseActions.length;
   for (var i:int = 0; i < count; i++)
   {
    FalseActions[i].Recycle();
   }
   
   count = FalseConditions.length;
   for (var i:int = 0; i < count; i++)
   {
    FalseConditions[i].Recycle();
   }
   
   TrueActions.length = 0;
   FalseActions.length = 0;
   TrueConditions.length = 0;
   FalseConditions.length = 0;
  }
 
 }
Finally we have our recycle method, pretty essential to my system, we have to make sure to recycle absolutely every single behavior and condition, for reuse. And this concludes the BaseCondition, it is not complicated but we haven't really seen it in action, in order to make this work we require a "ICondition" as well as another "IBehavior" to append to one of the action branches, let's start with the condition:
public class Con_CollidedWith implements ICondition
 {
  static var Head:Con_CollidedWith;
  var Next:Con_CollidedWith;
  var Self:Con_CollidedWith;
  
  var Target:PhysicsComponent; //Contains all the physics info.

  
  public function Con_CollidedWith()
  {
   Self = this;
  }
  
  public static function Create(Target:PhysicsComponent):Con_CollidedWith
  {
   var Vessel:Con_CollidedWith;
   if(Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new Con_CollidedWith();
   }
   
   Vessel.Target = Target;

   return Vessel;
  }

This is pretty much the same pattern I follow for almost everything that requires to be pooled, in there you can see an object of type of "PhysicsComponent", in there I store variables that represent the entity in the physical world, such as position, body type (Rectangle, poly, circle, etc), a collision list, etc.
  public function CheckCondition():Boolean
  {
   return Target.CollisionList.length > 0;
  }
  
  
  public function Recycle():void
  {
   Next = Head;
   Head = Self;
  }
  
 }
Finally we have the CheckCondition (which is part of the ICondition interface) where I test the condition at hand, in this case if we have something in our collision collection (meaning we have collided with something in the collision phase) it will return true, otherwise false. That concludes an example of a condition, you can create all sort of conditions, you can test if an enemy has certain amount of HP, or if a button has been clicked, etc. Now we move to the last piece of the puzzle, the Action, for this example I will use a function proxy action:
public class Act_ExecuteFunction implements IBehavior
 {
  static var Head:Act_ExecuteFunction;
  var Next:Act_ExecuteFunction;
  var Self:Act_ExecuteFunction;
  
  var Execute:Function;
  
  public function Act_ExecuteFunction()
  {
   Self = this;
  }
  
  public static function Create(Execute:Function):Act_ExecuteFunction
  {
   var Vessel:Act_ExecuteFunction;
   if(Head != null)
   {
    Vessel = Head;
    Head = Head.Next;
   }
   else
   {
    Vessel = new Act_ExecuteFunction();
   }
   
   Vessel.Execute = Execute;
   
   return Vessel;
  }
  
  public function Update():void
  {
   Execute.call();
  }
  
  
  public function Recycle():void
  {
   Execute = null;
   Next = Head;
   Head = Self;
  }
  
 }
As you can see in our Update function (this will only get called if the condition fell in the action branch where this action resided in) we call the function we appended to this action, much like a delegate. But we still don't know how to put all of this together, so allow me to make an example, in my current game there is "Honey" in the air and you require to collide with it in order to acquire it, here is how I make a honey:
public static function CreateHoneyItem(X:Number, Y:Number):void
  {
   var Honey:Entity = Entity.Create(World.InGameGroup);
   var Graphics:GraphicsComponent = GraphicsComponent.Create(Honey, AnimationLibrary.HONEY_GRAPHICS);
   var Physics:PhysicsComponent = PhysicsComponent.Create(Honey, X, Y, PhysicsComponent.ITEM);
   Physics.TurnIntoCircle(16);
   
   var Drawer:B_Draw = B_Draw.CreateOwnerless(1, Graphics, Physics); //gets recycled upon collision by the honey collision response
   
   var Condition_Collision:BaseCondition = BaseCondition.CreateRoot(Honey,Con_CollidedWith.Create(Physics));
   Condition_Collision.TrueActions.push(Act_ExecuteFunction.Create(HoneyCollisionResponse));

}

public static function HoneyCollisionResponse():void
{
    //Do your response here
}


So in order to make a honey I call the factory method for the honey, this will create a honey, and give it's behavior. The behavior says that it will Draw (B_Draw), Also says that if the honey collides with something then execute the function proxy (HoneyCollisionResponse).

There's more behind the scene, such as the collision system, the render system, etc, but it is not hard to get the idea on how the behavior tree works. Using this you can come up with all sort of complex behaviors, but the main issue with this is that they are inflexible upon creation, there won't be learning of AI, it will just run the routine over and over, but that doesn't have to be that way, there are methods to make this work the way you want to, implementing weight based decisions instead of just conditionals could be a way, there's a lot of things that can be implementing using this Behavior Tree pattern, but that would be another post for another time.

No comments:

Post a Comment