Problem with sequence,animation and AI behaviour

I am trying to define a sequence of behaviours involving AI behaviour using panda sequence class. First I define a walk behaviour using seek function and I activate obstacle avoidance. Next I define fall behaviour . Each behaviour has its own animation.when running the code, there are four main problems:
1- The animation of the second behaviour is activated , but the first one doesn’t
2- The behaviour is different each time I run the code
3- I cannot set the time of each action on the sequence
4- The seek behaviour may not stop if the actor was not able to reach target

I am still a beginner and it’s my first time using the engine, so may be I did something wrong.
Thanks in advance.

import math
import sys
import time

from direct.actor.Actor import Actor, CollisionSphere
from direct.interval.FunctionInterval import Func, Wait
from direct.interval.MetaInterval import Sequence, Parallel

from direct.showbase.InputStateGlobal import inputState
from direct.showbase.ShowBase import ShowBase, CollisionNode, CollisionTraverser, CollisionHandlerEvent, CollisionRay, \
    CollisionHandlerFloor, NodePath
from direct.showbase.ShowBaseGlobal import globalClock
from direct.interval.ActorInterval import ActorInterval
from panda3d.ai import AIWorld, AICharacter

from panda3d.core import AmbientLight, CollisionBox, CollisionCapsule, CollisionPlane, Plane
from panda3d.core import DirectionalLight
from panda3d.core import Vec3
from panda3d.core import Vec4
from panda3d.core import Point3
from panda3d.core import TransformState
from panda3d.core import BitMask32
from panda3d.core import Filename
from panda3d.core import PNMImage
from panda3d.physics import ForceNode, LinearVectorForce, ActorNode, PhysicsCollisionHandler


class MObj(object):
    def __init__(self,name,loader):
        self.name = name
        m = loader.loadModel("./models/cube/cube")
        self.model=m
        self.add_bounding_box()
        self.add_collision_ray()



    #
    #
    # def add_bounding_box(self):
    #     min,max = self.model.getTightBounds()
    #     sz = max-min
    #     # cs = CollisionBox(Point3(0,0,0),sz.x//2,sz.y//2,sz.z//2)
    #     cs = CollisionBox(Point3(0,0,0),2.0,2.0,2.0)
    #     self.cnodepath = self.model.attachNewNode(CollisionNode(self.name))
    #     self.cnodepath.node().addSolid(cs)
    #     self.cnodepath.show()
    #
    #     self.cnodepath.node().setFromCollideMask(0)
    #
    #     self.cnodepath.node().addSolid(CollisionRay(0, 0, 0, 0, 0, -1))



    def add_bounding_box(self):
        min,max = self.model.getTightBounds()
        sz = max-min
        # cs = CollisionBox(Point3(0,0,0),sz.x//2,sz.y//2,sz.z//2)
        cs = CollisionBox(Point3(0,0,0),2.0,2.0,2.0)
        self.cnodepath = self.model.attachNewNode(CollisionNode(self.name))
        self.cnodepath.node().addSolid(cs)
        self.cnodepath.show()
        self.cnodepath.node().setFromCollideMask(0)


    def add_collision_ray(self):

        self.raynodepath = self.model.attachNewNode(CollisionNode(self.name+str(1)))


        self.raynodepath.node().addSolid(CollisionRay(0, 0, 0, 0, 0, -1))



    def set_pos(self,pos):
        self.model.set_pos(pos)

    def set_scale(self,scale):
        self.model.set_scale(scale)

    def get_pos(self):
        return self.model.get_pos()

    def reparent(self,parent):
        self.model.reparentTo(parent)

class Character(object):

    def __init__(self,name,gravity):
        self.name = name
        self.speed = 20
        self.mass = 100


        self.actorNP = Actor('models/boy3/jack_walk.egg', {
            'stand': 'models/boy3/jack-stand.egg',
            'fall':'models/boy3/jack-fall.egg',
            'walk':'models/boy3/jack_walk-Armature|mixamo.com|Layer0.007.egg'})
        self.add_physics(gravity)
        self.add_bounding_box()
       # self.add_ai_behavior()












    def add_ai_behavior(self):
        self.AIChar = AICharacter(self.name,self.anp,100,0.05,5)



    def add_bounding_box(self):
        min,max = self.actorNP.getTightBounds()
        vec = max-min
        self.boundingX = (vec).x
        self.boundingY = (vec).y
        self.boundingZ = (vec).z


        self.cnp=self.actorNP.attachNewNode(CollisionNode(self.name))

        self.cnp.node().addSolid(CollisionSphere(Vec3(0.0,0.0,0.0),1.0))
        #self.cnp.node().addSolid(CollisionSphere(Vec3(self.boundingX/2,self.boundingY/2,self.boundingZ/2),self.boundingX))
        #self.cnp.node().addSolid(CollisionCapsule(min,max,0.5))
        self.cnp.show()
        #cnp.node().setFromCollideMask(BitMask32.bit(2))
        #self.cnp.node().setIntoCollideMask(0)

    def set_pos(self,pos): #basically sets the position of the physics node
        self.anp.set_pos(pos)

    def set_scale(self,scale):
        self.actorNP.set_scale(scale)

    def get_pos(self):
        return self.anp.get_pos()

    def add_physics(self,gravity):

        self.an = ActorNode()



        self.an.getPhysical(0).addLinearForce(gravity)

    def get_speed(self):
        return self.speed




#last thing to be done
    def reparent(self,parent):
        self.anp = parent.attachNewNode(self.an)
        self.actorNP.reparentTo(self.anp)

    def do_animation_sequence(self):
        self.actorNP.loop("walk")

        ai_behaviour = self.AIChar.getAiBehaviors()
        ai_behaviour.seek(Point3(1,5,-5))
        ai_behaviour.obstacleAvoidance(5.0)




class PhysicsManager(object):

    def __init__(self):

        self.floor_mask = BitMask32.bit(1)
        self.off_mask = BitMask32.allOff()


        self.traverser = CollisionTraverser('MainCollsionTraverser')
        self.pusher = PhysicsCollisionHandler()
        self.pusher.addInPattern("into-%in")
        self.pusher.addOutPattern("out-%in")
        self.floor_handler = CollisionHandlerFloor()








    def get_collision_traverser(self):
        return self.traverser

    def attach_collision_node_to_pusher_handler(self,cnp,node):
        self.pusher.addCollider(cnp,node)


    def get_pusher_handler(self):

        return self.pusher

    def attach_collision_node_to_floor_handler(self,cnp,node):
        self.floor_handler.addCollider(cnp,node)

    def get_floor_handler(self):
        return self.floor_handler

#
# class Action(object):
#
#     def __init_(self,actors,type,name):
#         self.type = type
#         self.name = name
#         self.actors = actors
#         if self.type == "inplace":
#             self.time = 20
#         elif self.type == "sound":
#             self.time = 40
#             pass
#             #shall depend on the length of the voice record
#         else:  #translational
#             if(len(self.actors)==2):
#                 pos1 = self.actors[0].get_pos()
#                 pos2 = self.actors[1].get_pos()
#                 dist = math.abs(pos1.x-pos2.x)+math.abs(pos1.y-pos2.y)
#                 speed = self.actor[0].get_speed()
#                 self.time = dist//speed + 5
#
#     def get_time(self):
#         return self.time
#
#



class walk_towards(object):

     def __init__(self,actor1,object):
       self.actor1 = actor1
       self.object = object
       self.pos1 = actor1.get_pos()
       self.pos2 = object.get_pos()
       dist = abs(self.pos1.x-self.pos2.x)+abs(self.pos1.y-self.pos2.y)
       speed = actor1.get_speed()
       self.time = dist//speed + 5



     def get_time(self):
         return self.time

     def play(self):

         #check if actor has the walk animation implemented
         self.actor1.actorNP.loop("walk")

         print("walking")

         ai_behaviour = self.actor1.AIChar.getAiBehaviors()
         ai_behaviour.seek(self.pos2-(1,1,0))
         ai_behaviour.obstacleAvoidance(2.0)


class fall_down(object):

     def __init__(self,actor1):
        self.actor = actor1

     def play(self):
        self.actor.actorNP.play("fall")





















class Scene_Manager(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        base.setBackgroundColor(0.1, 0.1, 0.8, 1)
        base.setFrameRateMeter(True)

        base.cam.setPos(0, 0, 40)
        base.cam.setHpr(-90,0,0)
        base.cam.lookAt(0, 0, 0)

        #Physics handling
        self.set_up_physics()
        self.PM = PhysicsManager()
        base.cTrav = self.PM.get_collision_traverser()

        #set up ai
        self.set_up_ai()

        #Create tasks
        self.taskMgr.add(self.update_ai_world,"updateAI")


        #add ground
        self.add_ground()

        #add objects in different positions on the map
        self.object1 = MObj("Box1",self.loader)
        self.object1.set_pos(Point3(0,0,-5))
        self.object1.reparent(self.render)
        self.object1.set_scale(0.5)
        self.attach_object_to_physics_world(self.object1)
        self.AIWorld.addObstacle(self.object1.cnodepath)

        self.object2 = MObj("Box2",self.loader)
        self.object2.set_pos(Point3(3,0,-5))
        self.object2.reparent(self.render)
        self.object2.set_scale(0.5)
        self.attach_object_to_physics_world(self.object2)
        self.AIWorld.addObstacle(self.object2.cnodepath)

        self.object3 = MObj("Box3",self.loader)
        self.object3.set_pos(Point3(6,0,-5))
        self.object3.reparent(self.render)
        self.object3.set_scale(0.5)
        self.AIWorld.addObstacle(self.object3.cnodepath)
        self.attach_object_to_physics_world(self.object3)

        self.object4 = MObj("Box4",self.loader)
        self.object4.set_pos(Point3(-3,0,-5))
        self.object4.reparent(self.render)
        self.object4.set_scale(0.5)
        self.attach_object_to_physics_world(self.object4)
        self.AIWorld.addObstacle(self.object4.cnodepath)

        self.object5 = MObj("Box5",self.loader)
        self.object5.set_pos(Point3(-5,-0,-5))
        self.object5.reparent(self.render)
        self.object5.set_scale(0.5)
        self.attach_object_to_physics_world(self.object5)
        self.AIWorld.addObstacle(self.object5.cnodepath)

        self.object6 = MObj("Box6",self.loader)
        self.object6.set_pos(Point3(-6,0,-5))
        self.object6.reparent(self.render)
        self.object6.set_scale(0.5)
        self.attach_object_to_physics_world(self.object6)
        self.AIWorld.addObstacle(self.object6.cnodepath)
        #add Actors to the scene
        self.actor1 = Character("Jack",self.gravityForce)

        self.actor1.reparent(self.render)
        self.actor1.add_ai_behavior()
        self.attach_node_to_physics_world(self.actor1)
        self.AIWorld.addAiChar(self.actor1.AIChar)
        self.actor1.set_pos(Point3(1,-5,-5))
        self.actor1.set_scale(2.0)
        #self.actor1.do_animation_sequence()



        self.walk = walk_towards(self.actor1,self.object1)
        self.fall = fall_down(self.actor1)


        self.seq = Sequence(Parallel(Func(self.walk.play),Wait(self.walk.get_time()+10)),Func(self.fall.play))
        #
        #seq = Sequence(Func(walk.play),Wait(walk.get_time()),Func(fall.play))
        #seq = Sequence(Parallel(Func(walk.play),walk.get_time()),Func(fall.play))
        self.seq.start()




        #self.taskMgr.add(self.AIbehav,"behave")

        #actor1.do_animation_sequence()
        # myInterval1 = actor1.actorNP.actorInterval(
        #                 "walk"
        #
        #
        #
        #
        #             )
        # myInterval2 = actor1.actorNP.actor_interval(
        #     "fall"
        # )
        #
        #
        # myInterval3 = actor1.actorNP.actor_interval("stand")
        #
        # seq = Sequence()
        # seq.append(myInterval1)
        # seq.extend([myInterval2,myInterval3])
        # seq.loop()




        self.taskMgr.add(self.check_state,"check")



        # Add collision handler events
        self.accept("into-%s"%self.object1.name,self.collide)





    # def AIbehav(self,task):
    #     print(self.ai_behaviour.behaviorStatus("seek"))
    #     return task.cont


    def check_state(self,task):
        if(self.actor1.AIChar.getAiBehaviors().behaviorStatus("seek")=="done"):

            print("done")
          #  self.actor1.AIChar.getAiBehaviors().removeAi("seek")

        return task.cont



    def collide(self,col_entry):
        print(col_entry.getFromNodePath().getParent().getParent().node().getChildren()[0])
       # col_entry.getFromNodePath().getParent().node().play("fall")
        #col_entry.getFromNodePath().getParent().stop()

    def attach_node_to_physics_world(self,actor1):
        base.physicsMgr.attachPhysicalNode(actor1.an)
        self.PM.attach_collision_node_to_pusher_handler(actor1.cnp,actor1.anp)
        base.cTrav.addCollider(actor1.cnp,self.PM.get_pusher_handler())

    def attach_object_to_physics_world(self,object):

        self.PM.attach_collision_node_to_floor_handler(object.raynodepath,object.model)
        object.raynodepath.node().setFromCollideMask(self.PM.floor_mask)
        object.raynodepath.node().setIntoCollideMask(self.PM.off_mask)
        base.cTrav.addCollider(object.raynodepath,self.PM.get_floor_handler())





    def set_up_physics(self):
        base.enableParticles()
        gravityFN= ForceNode('world-forces')
        gravityFNP=self.render.attachNewNode(gravityFN)
        self.gravityForce=LinearVectorForce(0,0,-9.81) #gravity acceleration
        gravityFN.addForce(self.gravityForce)
        base.physicsMgr.addLinearForce(self.gravityForce)




    def set_up_ai(self):
        self.AIWorld = AIWorld(self.render)


    def update_physics_world(self):
        pass

    def update_ai_world(self,task):
        self.AIWorld.update()

        return task.cont



    def add_ground(self):
        ground = CollisionNode('ground')
        cs = CollisionPlane(Plane(Vec3(0, 0, 1), Point3(0, 0, -6)))
        ground.addSolid(cs)
        cnp2=self.render.attachNewNode(ground)
        cnp2.show()
        cnp2.node().setIntoCollideMask(BitMask32.allOn())







if __name__=="__main__":

    scene = Scene_Manager()
    scene.run()

Maybe you simplify the code to logical elements? You can also try FSM.

Regarding point (1), does the animation show up in PView? (To test this, open a terminal/console, navigate to the folder in which your model is located (or open the terminal there to start with), and enter “pview jack_walk.egg jack_walk-Armature|mixamo.com|Layer0.007.egg”. You should see your character walking.)

Regarding point (3), it occurs to me that the time taken to seek an object isn’t necessarily the displacement between starting-point and end-point divided by a given speed. If the character changes speed (perhaps the AI module applies acceleration of some sort?), or the path is particularly winding, then the time may differ noticeably from the result of displacement/speed.

Regarding point1 the animation is working if I change the

line
seq = Sequence(Func(walk.play),Func(fall.play))

to seq = Sequence(Func(walk.play),Wait(walk.get_time(),Func(Fall.play))
I actually see the walking behaviour followed by the falling behaviour,so that’s why I don’t understand what is happening

Regarding point 3 , I was thinking of a way to play the walk animation as long as the AI character hasn’t reached the destination, and then switch to another animation. I can’t find a way of doing that in panda without sequences and sequences require duration which I can’t properly set. I thought about referencing the node in the collision handler and stopping the animation as you can see here , but I couldn’t reach the actor node

  def collide(self,col_entry):
    print(col_entry.getFromNodePath().getParent().getParent().node().getChildren()[0])
   col_entry.getFromNodePath().getParent().node().play("fall")

In general, the panda has nothing to do with, it is not to blame that you cannot write logic. However, you ignore my suggestion.
https://www.panda3d.org/manual/?title=FSM_Introduction

I am not ignoring your suggestion. The problem is that I am not making a game I am using the engine for another project , so I am really trying to utilize the available features in the engine as it supports many of my requirements. The thing is that I am doing an application that converts text to 3d videos , as an input to panda 3d i will have a json file having actions and their actors. I will then have to construct the actors and their actions dynamically, so I can’t quite well model the transitions in the FSM because I am not expecting what to come. So, I thought about playing the actions using sequence and parallels, but it doesn’t seem to work well.

I then do not understand if there are challenges:

actor.play(‘Animation Name’)
actor.stop()

Why not use colision events to send a message at the moment when the actor’s sphere clashes with the point’s sphere?

As you can see in my comment above, I have tried doing something similar, but I couldn’t reach the actor model node because I have passed the actor physics node , not the model it self to the pusher collision handler. Moreover, even if I managed to do stop the walking animation in the collision handler and start the fall animation, there are still situations where I need to call AI behaviour in a sequence . Do you know whether this technique will work well or whether there is an easier way to do it using the engine.

Oh, there is a mistake. In fact, you need to use the collision sphere on the node, then you connect the model of the actor inside the sphere. And all actions are verified with the sphere.

In theory, you can use lists for assignments. Your task is similar to creating a timeline if I understand correctly. Bypassing which you can call the desired animations one after another.

It is better to create a dictionary with time stamps for the desired animation. For events we sort of concluded.

I am using the collision sphere on the physics actor node because that is what the physics collision handler expects as the I understood from the documentation here
anp = render.attachNewNode(ActorNode(‘actor’))
fromObject = anp.attachNewNode(CollisionNode(‘colNode’))
fromObject.node().addSolid(CollisionSphere(0, 0, 0, 1))
pusher = PhysicsCollisionHandler()
pusher.addCollider(fromObject, anp)

But if I call the animations one after the other , the engine only plays the last animation. I can’t actually understand what you mean.

I think it is logical that you should track the end of the started animations by the last frame or by the time I suggested with the dictionary. Only then run the next one.

What you are trying to do is the same:

Node.setColor((1, 1, 1, 1))
Node.setColor((1, 0 ,1, 1))
Node.setColor((0, 1, 1, 1))
Node.setColor((1, 1, 0, 1))

This is actually only the last color.

Ah, I see now! I misunderstood your question, then! My apologies.

In that case, the problem is that, if I’m not much mistaken, a “Func” returns immediately. That means that when you run your first sequence above, the walking animation is played, and then, on the very next frame, the falling animation is played.

Hmm… I have two suggestions here. The first is to use collision, as mentioned above–see my next response for that. The second is to use an update-task, and include logic in that to detect the various cases in which the animation should change.

Hmm… Looking at your code (and I may be missing something), it looks as though the collider should be the parent of the Actor.

However, Actors are a bit funny, so it may be that the NodePath that you get back from that isn’t an Actor-NodePath.

So, what I suggest is that you use a common solution for getting at an object associated with a collider-node: store a reference to that object in a “PythonTag” in the collider-node. That should allow you to access it quite easily. (You may want to consider referencing the “Character” object rather than the Actor: doing so would give you access to the Character’s other variables, alongside the Actor.)

There is a caveat, however! Be careful to clear the Python Tag once you’re done with the Character, to prevent the circular reference from interfering with automatic garbage collection!

That seems to be a good idea . I can try it out . Basically I will need to define a task that keeps track of the current running animation and checks whether the animation time is over. Thanks for the suggestion

Actually, in my code I have attached the collider node to the actor model node and attached the actor model node to the actor physics node
so my hierarchy is
scene -> actor physics node -> actor model node =>collision node
and what I pass to the physics collision handler is
collision node path and actor physics node path
isn’t that correct ??
I like your suggestion regarding the python tag. I shall give it a try. Thanks so much for your help and effort

It’s not a problem. :slight_smile:

Wait… What is the “physics” node? What physics does the “physics” NodePath take part in? Unless the “physics” NodePath is just the “handle” NodePath by which you move the character around…?

(I’ll confess that I find your current design a little… convoluted, so I’m not following the code very well at a glance, I fear.)

If the “physics” NodePath is just a handle by which to move the rest, then fair enough.

To clarify, you pass to the “pusher” the collider, and the node that you want the collider to move around.

A simple setup might be to have a model or Actor node, with the collider as a child of it. You then pass the collider and the model/Actor to the “pusher”. This results in it using the collider to detect collisions, and moving the model/Actor as a result of them.

But coming back to the parentage, you say that the collider is a child of the Actor. In the collision-event, the “from NodePath” provided by the collision-entry should be the collider, of which the Actor is the parent.

This may be correct if the skin of the actor with his sphere changes. However, if this does not change, the tree can be made flat. In addition, the collision sphere can perform a number of other utilitarian functions, such as defining entry to a zone.

I don’t think that you can combine a collision node with a model-node, even of the latter is non-animated–although I may be mistaken.

In any case, I didn’t mean that the collider ought to be the parent of the Actor, but rather that it looked as though the collider was the parent of the Actor, I think.

That said, it looks like I mistyped, and wrote the two nodes the wrong way around there! Indeed, it looks as though the Actor is currently the parent of the collider!

Sorry for the confusion–that was me making a mistake in my typing, I believe! ^^;;

Well, I proceeded from the fact that for each model there can be its own sphere or a set of geometry for determining collisions. While NodePath is a management node, you can connect models for management that have different geometry. A suitable collider is appropriately needed, so you are right in this case.

Yup: there’s a distinction between a model’s bounding-sphere (which is used for culling purposes) and a collision-sphere, I believe.