User Tools

Site Tools


public:t-vien-10-3:lab_5_materials

Lab 5 - Actors and State Machines

Before You Start

Interacting with a moving actor

In this exercise, you have to create a character that paces around while he is unaware of you. But as soon as you come within a certain range, he stops and looks at you. While the character is watching you, you can click on him to start a conversation with him. If you have entered a conversation, you can click on something else to stop talking. If at any point you leave the character, he goes back to his pacing around. These states and the various events that trigger transitions are shown in this figure.

  1. You can start with the following code skeleton:
    import direct.directbase.DirectStart
    from direct.showbase.DirectObject import DirectObject
    from pandac.PandaModules import *
    from direct.actor.Actor import Actor
    from direct.fsm import FSM
    from direct.interval.IntervalGlobal import *
    from direct.task.Task import Task
    from direct.gui.OnscreenText import OnscreenText
     
    from picker import Picker # Note: This module is in the Lab 5 Asset file (put in same folder)
     
    # TODO: Add a Character class
     
    # TODO: Add a Conversation class 
     
     
    # Movement keys
    key_map = (('s',['back',1]),('s-up',['back',0]),('w',['forward',1]),('w-up',['forward',0]),
               ('a',['left',1]),('a-up',['left',0]),('d',['right',1]),('d-up',['right',0]))
     
    class World(DirectObject):
     
        def __init__(self):
     
            # Set up movement keys
            self.keystate = {'back':0, 'forward':0, 'right':0, 'left':0}
            for key in key_map:
                self.accept(key[0],self.set_keystate,key[1])
            self.lasttime = 0
            taskMgr.add(self.move, "move")
     
            # Camera setup
            base.disableMouse()
            base.camera.setPos(0,-10, 2)  
            base.camLens.setFov(60)  
            base.camLens.setNear(0.01)
            base.setBackgroundColor(Vec3(0,0,0))
     
            # Background environment setup
            palace = loader.loadModel("models/palacedoors.egg")
            palace.reparentTo(render)
            palace.setPos(0,0,0)
            palace.setScale(1.5)
     
            # Add a character into the world
            ## TODO: person = Character(render)
     
            # TODO: Add collision detection
     
     
        def set_keystate(self, key, state):
            """ Set a stored state (0,1) for a specified key ('back','forward','right','left')"""
            self.keystate[key] = state
     
        def move(self, task):
            """ 
            Update camera position and/or rotation for all active key states.
            """
            elapsed = task.time - self.lasttime
            if self.keystate['back'] is 1:
                base.camera.setPos(camera,0,-7*elapsed,0)
            if self.keystate['forward'] is 1:
                base.camera.setPos(camera,0,7*elapsed,0)  
            if self.keystate['right'] is 1:
                base.camera.setH(camera.getH()-30*elapsed) 
            if self.keystate['left'] is 1:
                base.camera.setH(camera.getH()+30*elapsed) 
            self.lasttime = task.time
            return task.cont
     
    # Create a world
    w = World()
    # Start main loop
    run()
  2. Adding a Moving Character:
    Create a Character class derived from FSM.FSM (Finite State Machine). In the constructor, you first need to call the constructor of the parent like this:
    FSM.FSM.__init__(self,"Character")

    Then, create a new node called “mover” that will become the parent of the actor. Place the mover at (3,5,0). This extra node is not always needed, but it makes it possible to redefine the “front” of the model by rotating it with respect to this new model base (think of the mover as a platform that the actor is standing on). Now you should load the Actor from the model “models/FelOrcWarriorAxe.egg” with the animation 'anims' : “models/FelOrcWarriorAxe.egg” and attach it to the mover. Define the front facing of the actor by rotating it 90 degrees (with respect to the mover). Notice that the animations are stored in exactly the same file as the model in this case. Furthermore, all the different motions that the Orc can perform are bundled together into a single animation here (that's just the way that this model/animation was constructed). In order to play a particular motion sequence from this animation, you always specify the fromFrame and toFrame. So, to start the Orcs walking animation, call loop('anims',fromFrame=166, toFrame=193) on your actor. The fromFrame and toFrame values of other motions can be found inside the text file “FelOrcWarriorAxe.animsez”. Finally create a new Sequence of Lerp Intervals that first changes mover heading to 90 (in about 0.8 seconds) and then moves it to position (-3,5,0) in about 3 seconds. Rotate the mover back and bring it back to the original position. Now loop this interval and watch your actor pace around!

  3. Starting a State Machine:
    Create a state for pacing such that when he enters the state he paces around, but stops when he leaves the state. You do that by adding the methods enterPacing(self) and exitPacing(self). In the former, you loop the Walk animation like before, but instead of calling loop on your movement interval, call resume. This is so that he can continue moving from where he left off when he was interrupted. In the latter, you'll have to stop the walking animation, but instead of calling stop, which leaves him frozen at the first frame of the animation, you should start a new idle motion loop (fromFrame=3083, toFrame=3166). Create another state (with both enter and exit methods) called Noticing and for now, just print out a message on the screen. Test your state machine by for example accepting two different keystrokes, one which enters the Pacing state by calling self.request('Pacing') from its handler and another that enters the Noticing state with self.request('Noticing').
  4. Making the Character Look:
    Now add to the actor the ability to face you when his self.is_looking member variable is set to True. Simply do this by creating a task called look and inside it call lookAt(base.camera) on the nodepath of the mover, but only if self.is_looking is True. Now set the self.is_looking variable to False when you enter the Pacing state and True when you enter the Noticing state. Test that this works (try approaching him from different directions). One problem that you may notice is that your character faces the wrong way once he resumes his pacing after having looked at you. You can fix this with a member variable that stores the last facing angle of the pacing state when leaving it and sets it again when re-entering it. See if you can get it fixed that way.
  5. Triggering Transitions with Collisions:
    Let's have him change automatically from Pacing to Noticing when you approach him. You need to create two Collision Spheres. One under a new CollisionNode you create under your base.camera node, which you call 'avatar' and another under a new CollisionNode you create under your actor node, which you call 'sensor'. To refresh your memory, here is how you add a collision sphere to a new collision node:
    <colnodepath> = <modelnodepath>.attachNewNode(CollisionNode(<name>))
    <colnodepath>.node().addSolid(CollisionSphere(0,0,0,<radius>))

    Use a radius of 5 for the avatar sphere and 2 for the sensor sphere. Now that you have the two collision spheres set up, you need to create a collision handler that sends out the types of messages we want. Inside your World constructor, create a new CollisionHandlerEvent instance and then define these two event patterns:

    <collhandler>.addInPattern('%fn-into-%in')
    <collhandler>.addOutPattern('%fn-out-%in')

    Then set up the default collision traverser (base.cTrav) to use your avatar collision node (that you created above) as the from object and associate events its collision to the handler. Here's how that could be done:

    base.cTrav = CollisionTraverser('world traverser')
    base.cTrav.addCollider(self.avatar,<collhandler>)

    Inside you Character class, you can now accept the messages 'avatar-into-sensor' and 'avatar-out-sensor' that represent when the user enters the sensor sphere around the character and when he/she leaves the sphere, respectively. Create handlers for these messages (as methods of Character), which you can call approach_handler and leave_handler. In the former, you should have the character enter the Noticing state and in the latter the Pacing state. Make sure this works (remember you can make collision solids visible by calling the show() method on their nodes).

  6. Triggering Transitions with Selection:
    Let's have you be able to start talking to your character if you click on him while he notices you. To enabling the picking of 3D objects with your mouse, you can import the Picker class and simply initialize it inside your Character constructor like this:
    <mouse_picker>=Picker(<youractornodepath>)   
    <mouse_picker>.make_pickable(<youractornodepath>)

    Now, whenever you click on your actor, an event with the name 'clicked_render/mover/Orc' occurs, which you can of course accept in your Character class. See if you can make your character respond to your clicking.

  7. Starting a Conversation:
    Create a new state called Conversing and enter that state when you click on your character in the Noticing state. Make sure the click is only received if the character is actually in the Noticing state (hint: you'll need to use the ignore method as well as the accept method). Similarly, only in the Conversing state should you receive the 'clicked_None' event (when user clicks outside of the character) and that should send the character back to Pacing. As for what happens in the Conversing state, you can use OnscreenText to display a greeting while you are in this state (and remove it when you exit the state). Verify that the behavior of your character is following the original state diagram.
  8. Simple Conversation Machine:
    Instead of just showing a greeting when you enter the Conversing state, let's start an actual dialog! Use the "FSM with input" method (and a single defaultFilter) to create a new Conversation class that handles the branching dialog shown in this diagram:
    The diagram shows the state names in the blue boxes and the text that should shown on the screen when entering each state. The user picks response '1' by pressing the '1' key on the keyboard, which then takes the conversation to the state connected by the arrow labeled '1'. Same for '2'. The simplest way to do this, is to have the new class (that inherits from FSM.FSM) accept '1' and '2' as keyboard events, which are handled by the same handler method that directly calls the self.request(<input>) method for advancing to the next state.
/var/www/ailab/WWW/wiki/data/pages/public/t-vien-10-3/lab_5_materials.txt · Last modified: 2010/10/14 02:20 by hannes