====== Lab 5 - Actors and State Machines ====== ===== Before Coming to Class ===== * Make sure you can run the Panda 3D sample programs on your machine ===== Useful Links ===== * [[http://www.ru.is/kennarar/hannes/useful/PQR2.3.html|Python Quick Reference (LOCAL)]] * [[http://www.ru.is/kennarar/hannes/useful/Python-Docs-2.5|Complete Python Documentation (LOCAL)]] //This is the latest 2.5 release, but new 2.5 features are well marked// * [[http://www.ru.is/kennarar/hannes/useful/PandaManual|Panda 3D Manual (LOCAL)]] * [[http://www.ru.is/kennarar/hannes/useful/PandaReference|Panda 3D Reference(LOCAL)]] * [[http://www.ru.is/kennarar/hannes/useful/BlenderManual/htmlI|Blender Manual (LOCAL)]] * [[http://www.ru.is/kennarar/hannes/useful/BlenderManual/htmlII/p2.html|Blender Reference (LOCAL)]] * [[http://www.ru.is/kennarar/hannes/share/chicken_export1.0.zip|Chicken 1.0 EGG Exporter for Blender 3D (LOCAL)]] ===== In-Class Excercises ===== ==== Before You Start ==== * Download and unzip the [[http://www.ru.is/kennarar/hannes/classes/ve2007/Lab5Assets.zip|Lab 5 Asset File]] into your working directory ==== 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 events that trigger transitions are shown in this figure.\\ {{public:t-vien-07-1:ve-lab5-states.jpg|}} You will need the following modules imported into your program: 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 - **Setting up:**\\ Create a **World** class derived from **DirectObject** like before and set up a camera at **(0,-70,2)** with FOV of **50** and a near clipping plane of **0.01**. Load and display the environment model called **environment** (it comes standard with Panda 3D, so you don't need any path information) and scale it down to **30%**. Add your camera movement code from [[Lab 2 Materials|Lab 2]]. Verify that you can now move around the landscape. - **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 you should load an **[[http://www.ru.is/kennarar/hannes/useful/PandaManual/Loading_Actors_and_Animations.1.html|Actor]]** from the model **"Models/eve"** with the animation **'Walk':'Models/eve_walk'"**. Place the actor at **(0,-20,0)** and scale it down to **80%**. Start your actor's **Walk** animation by calling **loop("Walk")** on your instance of it. Now create a new **Sequence** of [[http://www.ru.is/kennarar/hannes/useful/PandaManual/Lerp_Intervals.1.html|Lerp Intervals]] that first changes your actor's heading to **-90** (in about 0.8 seconds) and then moves him to position **(-10,-20,0)** in about 9 seconds. Rotate back and move back to the original position. Now loop this interval and watch your actor pace around! - **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 call **pose('Walk',X)** where X is a number of a frame you choose so that he looks like he's standing (try a few frames, starting at 2). 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')**. - **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 actor, but only if **self.is_looking** is **True**. Unfortunately the actor model is turned 180 degrees the wrong way, so we need to flip him over by then adding **180** degrees to his new heading (you can use **getH** and **setH**). 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). - **Fixing the stuck looking:**\\ 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 facing angle of the previous pacing state. See if you can get it fixed that way. - **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 **10** for the avatar sphere and **35** 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). - **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/Eve'** occurs, which you can of course **accept** in your **Character** class. See if you can make your character respond to your clicking. - **Context sensitive messaging:**\\ 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 on nothing) 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.