Pascal behavior tree implementation?

It seems like behavior trees are the way to go to control my animals. I see some interesting articles outlining their logic and structure.

Has anyone implemented behavior trees? I see there are java, c++ and python libraries.

I have the basics of a behavior tree working to make the pig behave as I had it doing in pascal based logic. Swim up if under water. Swim forward until you reach land if in water. Otherwise walk forward. This uses the sequence and selector nodes as described in the first link. I find it hard to think in this ‘tree logic’ way, but it seems like it will work well, especially for spreading looped functionality between ticks. I will clean up the code and post the basics.

If anybody is interested in such things, here are functioning baseclasses for a rudimentary Behavior Tree based primarily on the information in the first link of my initial post.

unit BehaviorTree;

{$mode ObjFPC}{$H+}

{ Base Behavior Tree Classes
  rudimentary level implementation }

{ based on ideas from primarily the first of these links }
{ https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work#close-modal }
{ https://robohub.org/introduction-to-behavior-trees/ }

{ Erik Donovan Johnson [email protected]
  Free to use and modify in any way you want.
  Example code. No warranty.  Use at your own risk. }

interface

uses Classes;

{ behavior status values }
const behavior_notrun  = 0;
      behavior_running = 1;
      behavior_success = 2;
      behavior_fail    = 3;

type TBehaviorStatus = integer;

     TBehaviorNode = class; { forward }
     { low level stack }
     TBehaviorDataStack = class { for storing stack }
        procedure Push(item : tbehaviornode);
        function Pop : tbehaviornode;
        private
        stack : array of tbehaviornode;
      end;

     { manages the running, data and active state of a behavior tree }
     TBehaviorRunner = class; { forward }

     { root behavior tree class that everything derives from }
     TBehaviorNode = class

       laststatus : TBehaviorStatus;

       constructor create;
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; virtual;
       procedure reset; dynamic;

     end;

    { single child behavior node that can modify the results of that child in subclasses }
    TBehaviorDecorator = class( TBehaviorNode )
       child : TBehaviorNode;
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
       procedure reset; override;
     end;

    { multichild behavior node.  how those children are processed depends on subclasses }
    TBehaviorComposite = class( TBehaviorNode )
       childindex : integer;
       children : array of TBehaviorNode;
       constructor create;
       destructor destroy; override;
       procedure reset; override;
       procedure add( item : TBehaviorNode );
     end;

    { sublass this to make checks and actions in overloaded Run methods }
    TBehaviorLeaf = class( TBehaviorNode )

     end;

{ composite nodes }

    { Sequence : visit each child in order, starting with the first, and when that
      succeeds will call the second, and so on down the list of children.
      If any child fails it will immediately return failure to the parent. If the
      last child in the sequence succeeds, then the sequence will return success to
      its parent. }

    TBehaviorSequence = class( TBehaviorComposite )
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
     end;

    { Selector : Unlike sequence which is AND, requiring all children to succeed to
      return a success, a selector will return a success if any of its children
      succeed and not process any further children. It will process the first child,
      and if it fails will process the second, and if that fails will process the third,
      until a success is reached, at which point it will instantly return success.
      It will fail if all children fail. This means a selector is analagous with an
      OR gate, and as a conditional statement can be used to check multiple conditions
      to see if any one of them is true. }

    TBehaviorSelector = class( TBehaviorComposite )
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
     end;

{ keep track of active behavior node and other info (in subclass) }
TBehaviorRunner = class
   RootNode   : TBehaviorNode;
   ActiveNode : TBehaviorNode;
   constructor create( iRootNode : TBehaviorNode );
   destructor destroy; override;
   function RunNext( secondspassed : single ) : TBehaviorStatus;
   { all Run calls must call this when finished with a success or failure to update the active node from the stack }
   procedure ActiveNodeFinishedRun;
   private
   DataStack  : TBehaviorDataStack;
 end;

implementation  //==============================================================

procedure TBehaviorDataStack.push( item : tbehaviornode );
var l : integer;
begin
  l := length( stack );
  setlength(stack, l + 1 );
  stack[l] := item;
end;

function TBehaviorDataStack.Pop : tbehaviornode;
var l : integer;
begin
  result := nil;
  l := length( stack );
  if l > 0 then
   begin
     dec( l );
     result := stack[l];
     setlength( stack, l );
   end;
end;

//--------------------------------

constructor TBehaviorNode.create;
 begin
   laststatus := behavior_notrun;
 end;

procedure TBehaviorNode.reset;
 begin
   laststatus := behavior_notrun;
 end;

function TBehaviorNode.Run(  runner : TBehaviorRunner;
                             secondspassed : single  ) : TBehaviorStatus;
 begin
   result := behavior_running;
 end;

//--------------------------------

function TBehaviorDecorator.run( runner : TBehaviorRunner;
                                 secondspassed : single ) : TBehaviorStatus;
 begin
   result := inherited;
   if assigned( child ) then
    begin
      runner.datastack.push( self ); { push this node as parent for the child, it will pop when done }
      runner.activenode := child;
      laststatus := child.run( runner, secondspassed );
    end;
   if laststatus <> behavior_running then { leaves active at deepest child that is running }
      runner.ActiveNodeFinishedRun;
 end;

procedure TBehaviorDecorator.reset;
 begin
   inherited;
   if assigned( child ) then
      child.reset;
 end;

//--------------------------------

constructor TBehaviorComposite.create;
 begin
   inherited;
   childindex := 0;
 end;

destructor TBehaviorComposite.destroy;
 var i : integer;
 begin
   for i := 0 to length( children ) - 1 do
      children[i].free;
   setlength( children, 0 );
 end;

procedure TBehaviorComposite.reset;
 var i : integer;
 begin
   inherited;
   childindex := 0;
   for i := 0 to length( children ) - 1 do
      children[i].reset;
 end;

procedure TBehaviorComposite.add( item : TBehaviorNode );
 var l : integer;
 begin
   l := length( children );
   setlength( children, l + 1 );
   children[l] := item;
 end;

//--------------------------------

function TBehaviorSequence.run( runner : TBehaviorRunner;
                                secondspassed : single ) : TBehaviorStatus;
 { will run one child per tick, finished with first child that is a fail }
 var childcount, i : integer;
     child : TBehaviorNode;
     childstatus : integer;
     childix : integer;
 begin
   if laststatus <= behavior_running then
    begin
      result := inherited;
      runner.activenode := self;
      childcount := length( children );
      if childindex < childcount then
       begin
         runner.datastack.push( self ); { push this node as parent for the child, it will pop when done }
         child := children[childindex];
         runner.activenode := child;
         ChildStatus := child.Run( runner, secondspassed );
         laststatus := childstatus; { running will continue waiting for child, fail will halt iteration with fail }
         if childstatus = behavior_success then
          begin
            inc( childindex );
            if childindex < childcount then
               laststatus := behavior_running; { keep looping until fail or finished }
          end;
       end
      else
         laststatus := behavior_fail;
      if laststatus <> behavior_running then { leaves active at deepest child that is running }
         runner.ActiveNodeFinishedRun;
    end;
   result := laststatus;
 end;

//--------------------------------

function TBehaviorSelector.run( runner : TBehaviorRunner;
                                secondspassed : single ) : TBehaviorStatus;
{ will run one child per tick, finished with first child that is a success }
 var childcount, i : integer;
     child : TBehaviorNode;
     childstatus : integer;
 begin
   if laststatus <= behavior_running then
    begin
      result := inherited;
      runner.activenode := self;
      childcount := length( children );
      if childindex < childcount then
       begin
         runner.datastack.push( self ); { push this node as parent for the child, it will pop when done }
         child := children[childindex];
         runner.activenode := child;
         ChildStatus := child.Run( runner, secondspassed );
         laststatus := childstatus; { running will continue waiting for child, success will halt iteration with success }
         if childstatus = behavior_fail then
          begin
            inc( childindex );
            if childindex < childcount then
               laststatus := behavior_running; { keep looping until success or finished }
          end;
       end
      else
         laststatus := behavior_fail;
    end;
   if laststatus <> behavior_running then { leaves active at deepest child that is running }
      runner.ActiveNodeFinishedRun;
   result := laststatus;
 end;

//------------------------------------

constructor TBehaviorRunner.create( iRootNode : TBehaviorNode );
 begin
   { unowned references to externally owned nodes }
   RootNode   := iRootNode;
   ActiveNode := RootNode;
   { owned datastack of unowned references to externally owned nodes }
   DataStack := TBehaviorDataStack.create;
 end;

destructor TBehaviorRunner.destroy;
 begin
   DataStack.Free;
 end;

function TBehaviorRunner.RunNext( secondspassed : single ) : TBehaviorStatus;
 begin
   result := rootnode.laststatus;
   if result <= behavior_running then
    begin
      if not assigned( activenode ) then
         activenode := rootnode;
      result := activenode.run( self, secondspassed ); { active node will change the the deepest running child }
      if result <> behavior_running then
         activenode := nil;
    end;
 end;

procedure TBehaviorRunner.ActiveNodeFinishedRun;
 begin
   activenode := DataStack.pop; { set active node to prior node in stack after success or fail }
 end;

end.

Then my simple pig behavior is set as follows using Behavior leaf subclasses. When it finishes, success or fail, it loops.

function initbehaviors : TBehaviorNode;
 var underwater: TBehaviorSequence;
     inwater : TBehaviorSequence;
     _or : TBehaviorSelector;
 begin
   underwater := TBehaviorSequence.create;
   underwater.add( TBehavior_AmIUnderWater.create );
   underwater.add( TBehavior_SwimUp.create );

   inwater := TBehaviorSequence.create;
   inwater.add( TBehavior_AmIInWater.create );
   inwater.add( TBehavior_Swim.create );

   _or := TBehaviorSelector.create;
   _or.add( underwater );
   _or.add( inwater );
   _or.add( TBehavior_moveonland_obstaclecheck.create );

   result := _or;
 end;

and then tie it to my castle game engine object with the TBehaviorRunner class which holds state for the behaviors. Then every frame I can call a RunNext(secondspassed) which will run the next active thing in the behavior tree.

Thank you for sharing!

Personally I did actually implement behavior trees long time ago in C#, for a Unity project in a company I worked for ~10 years ago. I found them a very useful concept, I felt behavior trees nicely allow to express various creature logic using typical OOP concepts. It was a good decision to use them, behavior trees allowed to easily adapt code to various experiments on the game design side – e.g. maybe some creature can do X before doing Y, maybe something could do Z from time to time… behavior trees allowed to express it nicely, without turning the code into a hard-to-maintain mess with giant “case / switch” statement :slight_smile:

It see the same benefits in your above code, cool. I.e. reading initbehaviors implementation is very easy, it is visible at a glance how pigs behave (and how to tweak it).

I plan to implement behavior trees in Pascal for creature logic in planned TCastleMoveAttack component (code in castle-engine/src/transform/castlebehaviors_moveattack.inc at master · castle-engine/castle-engine · GitHub , but it is commented out and doesn’t compile yet; and it doesn’t use behavior trees yet). The idea is to have a ready-to-use component to drive typical creatures in games, useful e.g. for examples/fps_game. Exposing behavior trees will allow to customize the behavior nicely,

1 Like

I cleaned it up more and got rid of the unneeeded laststatus and childindex, the latter I now store on the stack. This way a behavior tree can be shared between creatures as it doesn’t store any data about the individual instance, that is all in the behavior runner class. the data stack is now its own unit allowing stacking the parent node, the child index and now also the distance to move on land.

unit basedata;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils;

type

TBehaviorData = class
   private
   procedure setinteger( ivalue : integer ); virtual;
   function getinteger : integer; virtual;
   procedure setsingle( ivalue : single ); virtual;
   function getsingle : single; virtual;
   public
   property asinteger : integer read getinteger write setinteger;
   property assingle  : single read getsingle write setsingle;
 end;

TBehaviorInt = class( TBehaviorData )
   constructor create( ivalue : integer );
   private
   value : integer;
   procedure setinteger( ivalue : integer ); override;
   function getinteger : integer; override;
 end;

TBehaviorSingle = class( TBehaviorData )
   constructor create( ivalue : single );
   private
   value : single;
   procedure setsingle( ivalue : single ); override;
   function getsingle : single; override;
 end;


TBehaviorDataStack = class { for storing data }
    destructor destroy; override;
    procedure Push(item : tobject);
    function Pop : tobject;
    function IsEmpty : boolean;

    procedure pushint( i : integer );
    function popint : integer;
    function peekint : integer;  { look at what would have popped without popping }

    procedure pushsingle( i : single );
    function popsingle : single;
    function peeksingle : single;  { look at what would have popped without popping }

    private
    stack : array of tobject;
  end;

implementation

procedure TBehaviorData.setinteger( ivalue : integer );
 begin
 end;

function TBehaviorData.getinteger : integer;
 begin
   result := 0;
 end;

procedure TBehaviorData.setsingle( ivalue : single );
 begin
 end;

function TBehaviorData.getsingle : single;
 begin
   result := 0;
 end;

constructor TBehaviorInt.create( ivalue : integer );
 begin
   value := ivalue;
 end;

procedure TBehaviorInt.setinteger( ivalue : integer );
 begin
   value := ivalue;
 end;

function TBehaviorInt.getinteger : integer;
 begin
   result := value;
 end;

constructor TBehaviorSingle.create( ivalue : single );
 begin
   value := iValue;
 end;

procedure TBehaviorSingle.setsingle( ivalue : single );
 begin
   value := ivalue;
 end;

function TBehaviorSingle.getsingle : single;
 begin
   result := value;
 end;

//-------------------------------

destructor TBehaviorDataStack.destroy;
 var i : integer;
     item : TObject;
 begin
   for i := 0 to length( stack ) - 1 do
    begin
      item := stack[i];
      if Item is TBehaviorData then
         Item.Free;
    end;
 end;


procedure TBehaviorDataStack.Push(item : tobject);
 var l : integer;
 begin
   l := length( stack );
   setlength(stack, l + 1 );
   stack[l] := item;
 end;

function TBehaviorDataStack.Pop : tobject;
 var l : integer;
 begin
   result := nil;
   l := length( stack );
   if l > 0 then
    begin
      dec( l );
      result := stack[l];
      setlength( stack, l );
    end;
 end;

function TBehaviorDataStack.IsEmpty : boolean;
 begin
   result := length( stack ) > 0;
 end;

procedure TBehaviorDataStack.pushint( i : integer );
 begin
   push( TBehaviorInt.create( i ));
 end;

function TBehaviorDataStack.popint : integer;
 var item : tobject;
 begin
   result := 0;
   item := tobject( pop );
   if assigned( item ) then
    begin
      assert( item is TBehaviorInt ); { enforced types to be safer }
      result := TBehaviorData( item ).asinteger;
      item.free;
    end;
 end;

function TBehaviorDataStack.peekint : integer;  { look at what would have popped without popping }
 var l : integer;
     item : tobject;
 begin
    result := 0;
    l := length( stack );
    if l > 0 then
     begin
       dec( l );
       item := stack[l];
       if item is TBehaviorInt then
          result := TBehaviorData( item ).asinteger;
     end;
 end;

procedure TBehaviorDataStack.pushsingle( i : single );
 begin
   push( TBehaviorSingle.create( i ));
 end;

function TBehaviorDataStack.popsingle : single;
var item : tobject;
begin
  result := 0;
  item := tobject( pop );
  if assigned( item ) then
   begin
     assert( item is TBehaviorSingle ); { enforced types to be safer }
     result := TBehaviorData( item ).assingle;
     item.free;
   end;
end;

function TBehaviorDataStack.peeksingle : single;  { look at what would have popped without popping }
 var l : integer;
     item : tobject;
 begin
    result := 0;
    l := length( stack );
    if l > 0 then
     begin
       dec( l );
       item := stack[l];
       if item is TBehaviorSingle then
          result := TBehaviorData( item ).assingle;
     end;
 end;


end.                            

and then the main unit

unit BehaviorTree;

{$mode ObjFPC}{$H+}

{ Base Behavior Tree Classes
  rudimentary level implementation }

{ based on ideas from primarily the first of these links }
{ https://www.gamedeveloper.com/programming/behavior-trees-for-ai-how-they-work#close-modal }
{ https://robohub.org/introduction-to-behavior-trees/ }

{ Erik Donovan Johnson [email protected]
  Free to use and modify in any way you want.
  Example code. No warranty.  Use at your own risk. }

{ TBehaviorRunner keeps track of active node so doesn't have to traverse to find it.
  Designed so there a behavior tree can be shared by many
   objects without taking more data than the TBehaviorRunner and its data stack per object. }

interface

uses Classes,
     basedata;

{ behavior status values }
const behavior_notrun  = 0; { not used }
      behavior_running = 1;
      behavior_success = 2;
      behavior_fail    = 3;

type TBehaviorStatus = integer;

     TBehaviorNode = class; { forward }

     { manages the running, data and active state of a behavior tree }
     TBehaviorRunner = class; { forward }

     { root behavior tree class that everything derives from }
     TBehaviorNode = class

       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; virtual; abstract;

     end;

    { single child behavior node that can modify the results of that child in subclasses }
    TBehaviorDecorator = class( TBehaviorNode )
       child : TBehaviorNode;
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
     end;

    { multichild behavior node.  how those children are processed depends on subclasses }
    TBehaviorComposite = class( TBehaviorNode )
       children : array of TBehaviorNode;
       constructor create;
       destructor destroy; override;
       procedure add( item : TBehaviorNode );
       function RunNextChild( runner : TBehaviorRunner;
                              secondspassed : single ) : TBehaviorStatus;
     end;

    { sublass this to make checks and actions in overloaded Run methods }
    TBehaviorLeaf = class( TBehaviorNode )

     end;

{ composite nodes }

    { Sequence : visit each child in order, starting with the first, and when that
      succeeds will call the second, and so on down the list of children.
      If any child fails it will immediately return failure to the parent. If the
      last child in the sequence succeeds, then the sequence will return success to
      its parent. }

    TBehaviorSequence = class( TBehaviorComposite )
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
     end;

    { Selector : Unlike sequence which is AND, requiring all children to succeed to
      return a success, a selector will return a success if any of its children
      succeed and not process any further children. It will process the first child,
      and if it fails will process the second, and if that fails will process the third,
      until a success is reached, at which point it will instantly return success.
      It will fail if all children fail. This means a selector is analagous with an
      OR gate, and as a conditional statement can be used to check multiple conditions
      to see if any one of them is true.  Some people call this a Fallback. }

    TBehaviorSelector = class( TBehaviorComposite )
       function Run( runner : TBehaviorRunner;
                     secondspassed : single ) : TBehaviorStatus; override;
     end;

{ keep track of active behavior node and other info (in subclass) }
TBehaviorRunner = class
   RootNode   : TBehaviorNode;
   ActiveNode : TBehaviorNode;
   DataStack  : TBehaviorDataStack;
   constructor create( iRootNode : TBehaviorNode );
   destructor destroy; override;
   function RunTick( secondspassed : single ) : TBehaviorStatus;
   { all Run calls must call this when finished with a current status to update the active node from the stack as needed }
   procedure UpdateActiveRunStatus( istatus : TBehaviorStatus );
 end;

implementation  //==============================================================

function TBehaviorDecorator.run( runner : TBehaviorRunner;
                                 secondspassed : single ) : TBehaviorStatus;
 begin
   if assigned( child ) then
    begin
      runner.datastack.push( self ); { push this node as parent for the child, it will pop when done }
      runner.activenode := child;
      result := child.run( runner, secondspassed );
    end
   else
      result := behavior_fail;
   runner.UpdateActiveRunStatus( result );
 end;

//--------------------------------

constructor TBehaviorComposite.create;
 begin
   inherited;
 end;

destructor TBehaviorComposite.destroy;
 var i : integer;
 begin
   for i := 0 to length( children ) - 1 do
      children[i].free;
   setlength( children, 0 );
 end;

procedure TBehaviorComposite.add( item : TBehaviorNode );
 var l : integer;
 begin
   l := length( children );
   setlength( children, l + 1 );
   children[l] := item;
 end;

function TBehaviorComposite.RunNextChild( runner : TBehaviorRunner;
                                          secondspassed : single ) : TBehaviorStatus;
 var childcount, childix : integer;
     child : TBehaviorNode;
 begin
   childcount := length( children );
   childix := runner.datastack.peekint; { peek for childix on stack }
   if childix < childcount then
    begin
      if childix = 0 then
         runner.datastack.pushint( 0 ); { push 0 since it wasn't actually stored }
      runner.datastack.push( self ); { push this node as parent for the child, it will pop when done }
      child := children[childix];
      runner.activenode := child;
      result := child.Run( runner, secondspassed );
    end
   else
    begin
      assert( false ); { this should never happen }
      result := behavior_fail;
    end;
 end;

//--------------------------------

function TBehaviorSequence.run( runner : TBehaviorRunner;
                                secondspassed : single ) : TBehaviorStatus;
 { will run one child per tick, finished with first child that is a fail }
 var childcount : integer;
     childix : integer;
 begin
   runner.activenode := self;
   childcount := length( children );
   childix := runner.datastack.peekint; { peek for childix on stack }
   if childix < childcount then
    begin
      result := RunNextChild( runner, secondspassed );
      { running will continue waiting for child, fail will halt iteration with fail }
      case result of
       behavior_success :
         begin
           childix := runner.datastack.popint;
           inc( childix );
           if childix < childcount then
            begin
              runner.datastack.pushint(childix); { add child ix back to stack }
              result := behavior_running; { keep looping until fail or finished }
            end;
         end;
       behavior_fail : runner.datastack.popint;
       behavior_running :;
      end;
    end
   else
      result := behavior_fail;
   runner.UpdateActiveRunStatus( result );
 end;

//--------------------------------

function TBehaviorSelector.run( runner : TBehaviorRunner;
                                secondspassed : single ) : TBehaviorStatus;
{ will run one child per tick, finished with first child that is a success }
 var childcount : integer;
     childix : integer;
 begin
   runner.activenode := self;
   childcount := length( children );
   childix := runner.datastack.peekint; { peek for childix on stack }
   if childix < childcount then
    begin
      result := RunNextChild( runner, secondspassed );
      { running will continue waiting for child, success will halt iteration with success }
      case result of
       behavior_fail :
         begin
           childix := runner.datastack.popint;
           inc( childix );
           if childix < childcount then
            begin
              runner.datastack.pushint(childix); { add child ix back to stack }
              result := behavior_running; { keep looping until success or finished }
            end;
         end;
       behavior_success : runner.datastack.popint;
       behavior_running :;
      end;
    end
   else
      result := behavior_fail;
   runner.UpdateActiveRunStatus( result );
 end;

//------------------------------------

constructor TBehaviorRunner.create( iRootNode : TBehaviorNode );
 begin
   { unowned references to externally owned nodes }
   RootNode   := iRootNode;
   ActiveNode := RootNode;
   { owned datastack of unowned references to externally owned nodes }
   DataStack := TBehaviorDataStack.create;
 end;

destructor TBehaviorRunner.destroy;
 begin
   DataStack.Free;
 end;

function TBehaviorRunner.RunTick( secondspassed : single ) : TBehaviorStatus;
 begin
   if not assigned( activenode ) then
      activenode := rootnode;
   result := activenode.run( self, secondspassed ); { active node will change tthe the deepest running child }

   if result <> behavior_running then
    begin
      repeat until DataStack.Pop = nil; { clear stack even though it should already be clear }
      activenode := nil;
    end;
 end;

procedure TBehaviorRunner.UpdateActiveRunStatus( istatus : TBehaviorStatus );
 begin
   if istatus <> behavior_running then
    begin
      assert( DataStack.peekint = 0 );
      assert( DataStack.peeksingle = 0 );
      activenode := TBehaviorNode( DataStack.pop ); { set active node to prior node in stack after success or fail }
    end;
 end;

end.

I shared this, and icosphere (and my x3dtools that it now uses) in a public repo at GitHub - xistext/edjlib: Code I have shared on Castle Game Engine Forum.

I also added GPC.pas General Polygon Clipper. I ported to Delphi long long ago and used for decades in aviation software so it is solid. Uses all doubles, so not perfect for CGE but I still find it useful. It could be converted with some effort to use TVertex2. Lets you combine 2d polygons with add and subtract to create complex polygons with holes and islands, etc. It was the basis for the water layer in the gps software I wrote. I use it now to find out if my curved road shape 2d outline polygons intersects the 2d outline polygons of other roads or buildings in my current game.

Thank you! I updated Additional Components | Castle Game Engine to point to your repo:)

1 Like

:persevere:This behavior tree code is flawed. It fails to unwind the stack for nested composite nodes, so then fails to hit subsequent children as it starts returning statuses back up the tree. A fundamental flaw in my logic that doesn’t have a clear solution yet… So nobody should waste any time on this yet.

I have that problem solved and have updated the code on the repo. Part of my problem is that the flow of the tree is counterintuitive to me so my little behavior tree in initbehaviors was defined wrong for what I wanted to occur. I found this free online behavior tree visualizer helpful … Behavior Tree Visual Editor.

Now my pig UnderWater? Swimup. InWater? Swim to shore, walk a little and then wiggle (presumably to dry) works. I added a name field to the node to make debugging easier. It is defined like this

   underwater := TBehaviorSequence.create2( TBehavior_AmIUnderWater.create, { underwater? }
                                            TBehavior_SwimUp.create );      { swim up }

   underwater.name := 'underwater';
   inwater := TBehaviorSequence.create2( TBehavior_AmIInWater.create, { in water? }
                                         TBehavior_Swim.create );     { swim to land }
   inwater.Add( TBehavior_moveonland_obstaclecheck.create );          { move 5 units on land }
   inwater.Add( TBehavior_Wiggle.create );                            { wiggle to dry }
   inwater.name := 'inwater';

   _and := TBehaviorSequence.create2( underwater, inwater );
   result := _and;
1 Like

It is working great. Pig ran in circles all night long, swimming and wiggling to dry off as needed. 901699 ticks ran. My tree is getting a little bigger. I reworked some of the leaf functions to play nicer with the logic the tree sorta forces you to do.

abovewater := TBehaviorSelector.create2( TBehavior_AmIAboveWater.create, { above water | swim up   }
                                            TBehavior_SwimUp.create,
                                            'abovewater' );
   righttoland := TBehaviorSequence.create2( TBehavior_RightIsBetterForLand.create, { land is closer to right? }
                                             TBehavior_SwimTurn.create( true ),      { swim turn right }
                                             'righttoland' );
   righttoland := TBehavior_Forcefail.Create( righttoland ); { whether or not we turned, force fail so we continue seletor}

   dry := TBehaviorSequence.create2( TBehavior_moveonland_obstaclecheck.create, { move 1 unit on land }
                                     TBehavior_Wiggle.create, 'dry off' );      { wiggle to dry }
   outofwater := TBehaviorSelector.create2( TBehavior_AmIOutOfWater.create, { in water? }
                                             righttoland, { if land is closer to right, turn right }
                                             'outofwater' );
   outofwater.add(TBehavior_Inverter.Create( TBehavior_Swim.create(1) )); { swim 1 unit success if land or complete so will continue behavior cycle }
   outofwater.add( dry );

   safefromwater := TBehaviorSequence.create2( abovewater, outofwater, 'safefromwater' );

   { walk in circle }
   defaultbehavior := TBehaviorSequence.create2( TBehavior_Turn.create(true),
                                                 TBehavior_moveonland_obstaclecheck.create(0.1) );

   mainloop := TBehaviorSequence.create2( safefromwater, { only proceed if safe from water }
                                          defaultbehavior, 'mainloop' );

   result := mainloop;                            

Here is some debug output to get an idea of how it flows.

>-Tick18:MoveOnLand1[.89]: Running
>-Tick19:MoveOnLand1[.88]: Running
>-Tick20:MoveOnLand1[.87]: Running
>-Tick21:MoveOnLand1[.86]: Running
>-Tick22:MoveOnLand1[.85]: Running
>-Tick23:MoveOnLand1[]: Fail ]: Fail ]: Fail ]: Fail ]: Fail
>-Tick24:mainloop►[ child0>safefromwater►[ child0>abovewater?[ child0>AmIAboveWater[]: Fail ]: Running ]: Running ]: Running
>-Tick25:abovewater?[ child1>SwimUp[]: Running ]: Running
>-Tick26:SwimUp[]: Success ]: Success
>-Tick27:safefromwater►[ child1>outofwater?[ child0>AmIOutOfWater[]: Fail ]: Running ]: Running
>-Tick28:outofwater?[ child1>ForceFail[righttoland►[ child0>RightIsBetterForLand[]: Fail ]: Fail ]: Fail ]: Running
>-Tick29:outofwater?[ child2>Invert[Swim[1]: Running ]: Running ]: Running
>-Tick30:Swim[.99]: Running
>-Tick31:Swim[.99]: Running
>-Tick32:Swim[.99]: Running
>-Tick33:Swim[.98]: Running
>-Tick34:Swim[.98]: Running
>-Tick35:Swim[.98]: Running
>-Tick36:Swim[.98]: Running

was walking along then found itself underwater so failed out of its current node, back to the root … so swam up and started swimming ahead. The number in the brackets is the distance remaining before swim fails for not get to shore and the loop starts again (where it has a chance to turn right toward shore if closer). Reminds me of programming in Prolog back in the 80s… where one unexpected fail can really alter the expected results. Note that the arrow and the question mark indicate sequencer and selector. There are two decorators being used ‘ForceFail’ and ‘Invert’.

20 animals (ie running 20 ticks per frame) drops my framerate from 30+ fps to low 20fps. I can probably improve performance but will probably end up rationing ticks by LOD.