def __init__(self): self.keyMap = {"left":0, "right":0, "forward":0, "cam-left":0, "cam-right":0} base.win.setClearColor(Vec4(0,0,0,1)) # Post the instructions self.title = addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)") self.inst1 = addInstructions(0.95, "[ESC]: Quit") self.inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left") self.inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right") self.inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward") self.inst6 = addInstructions(0.70, "[A]: Rotate Camera Left") self.inst7 = addInstructions(0.65, "[S]: Rotate Camera Right") # Set up the environment # # This environment model contains collision meshes. If you look # in the egg file, you will see the following: # # <Collide> { Polyset keep descend } # # This tag causes the following mesh to be converted to a collision # mesh -- a mesh which is optimized for collision, not rendering. # It also keeps the original mesh, so there are now two copies --- # one optimized for rendering, one for collisions. self.environ = loader.loadModel("models/samples/roaming_ralph/world") self.environ.reparentTo(render) self.environ.setPos(0,0,0) # Create the main character, Ralph ralphStartPos = self.environ.find("**/start_point").getPos() self.ralph = Actor("models/samples/roaming_ralph/ralph", {"run":"models/samples/roaming_ralph/ralph_run", "walk":"models/samples/roaming_ralph/ralph_walk"}) self.ralph.reparentTo(render) self.ralph.setScale(.2) self.ralph.setPos(ralphStartPos) # Create a floater object. We use the "floater" as a temporary # variable in a variety of calculations. self.floater = NodePath(PandaNode("floater")) self.floater.reparentTo(render) # Accept the control keys for movement and rotation self.accept("escape", sys.exit) self.accept("arrow_left", self.setKey, ["left",1]) self.accept("arrow_right", self.setKey, ["right",1]) self.accept("arrow_up", self.setKey, ["forward",1]) self.accept("a", self.setKey, ["cam-left",1]) self.accept("s", self.setKey, ["cam-right",1]) self.accept("arrow_left-up", self.setKey, ["left",0]) self.accept("arrow_right-up", self.setKey, ["right",0]) self.accept("arrow_up-up", self.setKey, ["forward",0]) self.accept("a-up", self.setKey, ["cam-left",0]) self.accept("s-up", self.setKey, ["cam-right",0]) taskMgr.add(self.move,"moveTask") # Game state variables self.isMoving = False # Set up the camera base.disableMouse() base.camera.setPos(self.ralph.getX(),self.ralph.getY()+10,2) # We will detect the height of the terrain by creating a collision # ray and casting it downward toward the terrain. One ray will # start above ralph's head, and the other will start above the camera. # A ray may hit the terrain, or it may hit a rock or a tree. If it # hits the terrain, we can detect the height. If it hits anything # else, we rule that the move is illegal. self.cTrav = CollisionTraverser() self.ralphGroundRay = CollisionRay() self.ralphGroundRay.setOrigin(0,0,1000) self.ralphGroundRay.setDirection(0,0,-1) self.ralphGroundCol = CollisionNode('ralphRay') self.ralphGroundCol.addSolid(self.ralphGroundRay) self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0)) self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff()) self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol) self.ralphGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler) self.camGroundRay = CollisionRay() self.camGroundRay.setOrigin(0,0,1000) self.camGroundRay.setDirection(0,0,-1) self.camGroundCol = CollisionNode('camRay') self.camGroundCol.addSolid(self.camGroundRay) self.camGroundCol.setFromCollideMask(BitMask32.bit(0)) self.camGroundCol.setIntoCollideMask(BitMask32.allOff()) self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol) self.camGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)
def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText( text="Panda3D: Tutorial - Collision Detection", style=1, fg=(1, 1, 1, 1), pos=(0.7, -0.95), scale=.07, font=font) self.instructions = OnscreenText(text="Mouse pointer tilts the board", pos=(-1.3, .95), fg=(1, 1, 1, 1), font=font, align=TextNode.ALeft, scale=.05) self.accept("escape", sys.exit) #Escape quits base.disableMouse() #Disable mouse-based camera control camera.setPosHpr(0, 0, 25, 0, -90, 0) #Place the camera #Load the maze and place it in the scene self.maze = loader.loadModel("models/samples/ball_in_maze/maze") self.maze.reparentTo(render) #Most times, you want collisions to be tested against invisible geometry #rather than every polygon. This is because testing against every polygon #in the scene is usually too slow. You can have simplified or approximate #geometry for the solids and still get good results. # #Sometimes you'll want to create and position your own collision solids in #code, but it's often easier to have them built automatically. This can be #done by adding special tags into an egg file. Check maze.egg and ball.egg #and look for lines starting with <Collide>. The part is brackets tells #Panda exactly what to do. Polyset means to use the polygons in that group #as solids, while Sphere tells panda to make a collision sphere around them #Keep means to keep the polygons in the group as visable geometry (good #for the ball, not for the triggers), and descend means to make sure that #the settings are applied to any subgroups. # #Once we have the collision tags in the models, we can get to them using #NodePath's find command #Find the collision node named wall_collide self.walls = self.maze.find("**/wall_collide") #Collision objects are sorted using BitMasks. BitMasks are ordinary numbers #with extra methods for working with them as binary bits. Every collision #solid has both a from mask and an into mask. Before Panda tests two #objects, it checks to make sure that the from and into collision masks #have at least one bit in common. That way things that shouldn't interact #won't. Normal model nodes have collision masks as well. By default they #are set to bit 20. If you want to collide against actual visable polygons, #set a from collide mask to include bit 20 # #For this example, we will make everything we want the ball to collide with #include bit 0 self.walls.node().setIntoCollideMask(BitMask32.bit(0)) #CollisionNodes are usually invisible but can be shown. Uncomment the next #line to see the collision walls #self.walls.show() #We will now find the triggers for the holes and set their masks to 0 as #well. We also set their names to make them easier to identify during #collisions self.loseTriggers = [] for i in range(6): trigger = self.maze.find("**/hole_collide" + str(i)) trigger.node().setIntoCollideMask(BitMask32.bit(0)) trigger.node().setName("loseTrigger") self.loseTriggers.append(trigger) #Uncomment this line to see the triggers #trigger.show() #Ground_collide is a single polygon on the same plane as the ground in the #maze. We will use a ray to collide with it so that we will know exactly #what height to put the ball at every frame. Since this is not something #that we want the ball itself to collide with, it has a different #bitmask. self.mazeGround = self.maze.find("**/ground_collide") self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1)) #Load the ball and attach it to the scene #It is on a root dummy node so that we can rotate the ball itself without #rotating the ray that will be attached to it self.ballRoot = render.attachNewNode("ballRoot") self.ball = loader.loadModel("models/samples/ball_in_maze/ball") self.ball.reparentTo(self.ballRoot) #Find the collison sphere for the ball which was created in the egg file #Notice that it has a from collision mask of bit 0, and an into collison #mask of no bits. This means that the ball can only cause collisions, not #be collided into self.ballSphere = self.ball.find("**/ball") self.ballSphere.node().setFromCollideMask(BitMask32.bit(0)) self.ballSphere.node().setIntoCollideMask(BitMask32.allOff()) #No we create a ray to start above the ball and cast down. This is to #Determine the height the ball should be at and the angle the floor is #tilting. We could have used the sphere around the ball itself, but it #would not be as reliable self.ballGroundRay = CollisionRay() #Create the ray self.ballGroundRay.setOrigin(0, 0, 10) #Set its origin self.ballGroundRay.setDirection(0, 0, -1) #And its direction #Collision solids go in CollisionNode self.ballGroundCol = CollisionNode( 'groundRay') #Create and name the node self.ballGroundCol.addSolid(self.ballGroundRay) #Add the ray self.ballGroundCol.setFromCollideMask( BitMask32.bit(1)) #Set its bitmasks self.ballGroundCol.setIntoCollideMask(BitMask32.allOff()) #Attach the node to the ballRoot so that the ray is relative to the ball #(it will always be 10 feet over the ball and point down) self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol) #Uncomment this line to see the ray #self.ballGroundColNp.show() #Finally, we create a CollisionTraverser. CollisionTraversers are what #do the job of calculating collisions self.cTrav = CollisionTraverser() #Collision traverservs tell collision handlers about collisions, and then #the handler decides what to do with the information. We are using a #CollisionHandlerQueue, which simply creates a list of all of the #collisions in a given pass. There are more sophisticated handlers like #one that sends events and another that tries to keep collided objects #apart, but the results are often better with a simple queue self.cHandler = CollisionHandlerQueue() #Now we add the collision nodes that can create a collision to the #traverser. The traverser will compare these to all others nodes in the #scene. There is a limit of 32 CollisionNodes per traverser #We add the collider, and the handler to use as a pair self.cTrav.addCollider(self.ballSphere, self.cHandler) self.cTrav.addCollider(self.ballGroundColNp, self.cHandler) #Collision traversers have a built in tool to help visualize collisions. #Uncomment the next line to see it. #self.cTrav.showCollisions(render) #This section deals with lighting for the ball. Only the ball was lit #because the maze has static lighting pregenerated by the modeler lAttrib = LightAttrib.makeAllOff() ambientLight = AmbientLight("ambientLight") ambientLight.setColor(Vec4(.55, .55, .55, 1)) lAttrib = lAttrib.addLight(ambientLight) directionalLight = DirectionalLight("directionalLight") directionalLight.setDirection(Vec3(0, 0, -1)) directionalLight.setColor(Vec4(0.375, 0.375, 0.375, 1)) directionalLight.setSpecularColor(Vec4(1, 1, 1, 1)) lAttrib = lAttrib.addLight(directionalLight) self.ballRoot.node().setAttrib(lAttrib) #This section deals with adding a specular highlight to the ball to make #it look shiny m = Material() m.setSpecular(Vec4(1, 1, 1, 1)) m.setShininess(96) self.ball.setMaterial(m, 1) #Finally, we call start for more initialization self.start()
class World(DirectObject): def __init__(self): self.keyMap = {"left":0, "right":0, "forward":0, "cam-left":0, "cam-right":0} base.win.setClearColor(Vec4(0,0,0,1)) # Post the instructions self.title = addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)") self.inst1 = addInstructions(0.95, "[ESC]: Quit") self.inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left") self.inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right") self.inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward") self.inst6 = addInstructions(0.70, "[A]: Rotate Camera Left") self.inst7 = addInstructions(0.65, "[S]: Rotate Camera Right") # Set up the environment # # This environment model contains collision meshes. If you look # in the egg file, you will see the following: # # <Collide> { Polyset keep descend } # # This tag causes the following mesh to be converted to a collision # mesh -- a mesh which is optimized for collision, not rendering. # It also keeps the original mesh, so there are now two copies --- # one optimized for rendering, one for collisions. self.environ = loader.loadModel("models/samples/roaming_ralph/world") self.environ.reparentTo(render) self.environ.setPos(0,0,0) # Create the main character, Ralph ralphStartPos = self.environ.find("**/start_point").getPos() self.ralph = Actor("models/samples/roaming_ralph/ralph", {"run":"models/samples/roaming_ralph/ralph_run", "walk":"models/samples/roaming_ralph/ralph_walk"}) self.ralph.reparentTo(render) self.ralph.setScale(.2) self.ralph.setPos(ralphStartPos) # Create a floater object. We use the "floater" as a temporary # variable in a variety of calculations. self.floater = NodePath(PandaNode("floater")) self.floater.reparentTo(render) # Accept the control keys for movement and rotation self.accept("escape", sys.exit) self.accept("arrow_left", self.setKey, ["left",1]) self.accept("arrow_right", self.setKey, ["right",1]) self.accept("arrow_up", self.setKey, ["forward",1]) self.accept("a", self.setKey, ["cam-left",1]) self.accept("s", self.setKey, ["cam-right",1]) self.accept("arrow_left-up", self.setKey, ["left",0]) self.accept("arrow_right-up", self.setKey, ["right",0]) self.accept("arrow_up-up", self.setKey, ["forward",0]) self.accept("a-up", self.setKey, ["cam-left",0]) self.accept("s-up", self.setKey, ["cam-right",0]) taskMgr.add(self.move,"moveTask") # Game state variables self.isMoving = False # Set up the camera base.disableMouse() base.camera.setPos(self.ralph.getX(),self.ralph.getY()+10,2) # We will detect the height of the terrain by creating a collision # ray and casting it downward toward the terrain. One ray will # start above ralph's head, and the other will start above the camera. # A ray may hit the terrain, or it may hit a rock or a tree. If it # hits the terrain, we can detect the height. If it hits anything # else, we rule that the move is illegal. self.cTrav = CollisionTraverser() self.ralphGroundRay = CollisionRay() self.ralphGroundRay.setOrigin(0,0,1000) self.ralphGroundRay.setDirection(0,0,-1) self.ralphGroundCol = CollisionNode('ralphRay') self.ralphGroundCol.addSolid(self.ralphGroundRay) self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0)) self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff()) self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol) self.ralphGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler) self.camGroundRay = CollisionRay() self.camGroundRay.setOrigin(0,0,1000) self.camGroundRay.setDirection(0,0,-1) self.camGroundCol = CollisionNode('camRay') self.camGroundCol.addSolid(self.camGroundRay) self.camGroundCol.setFromCollideMask(BitMask32.bit(0)) self.camGroundCol.setIntoCollideMask(BitMask32.allOff()) self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol) self.camGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler) # Uncomment this line to see the collision rays #self.ralphGroundColNp.show() #self.camGroundColNp.show() #Uncomment this line to show a visual representation of the #collisions occuring #self.cTrav.showCollisions(render) #Records the state of the arrow keys def setKey(self, key, value): self.keyMap[key] = value # Accepts arrow keys to move either the player or the menu cursor, # Also deals with grid checking and collision detection def move(self, task): # Get the time elapsed since last frame. We need this # for framerate-independent movement. elapsed = globalClock.getDt() # If the camera-left key is pressed, move camera left. # If the camera-right key is pressed, move camera right. base.camera.lookAt(self.ralph) if (self.keyMap["cam-left"]!=0): base.camera.setX(base.camera, -(elapsed*20)) if (self.keyMap["cam-right"]!=0): base.camera.setX(base.camera, +(elapsed*20)) # save ralph's initial position so that we can restore it, # in case he falls off the map or runs into something. startpos = self.ralph.getPos() # If a move-key is pressed, move ralph in the specified direction. if (self.keyMap["left"]!=0): self.ralph.setH(self.ralph.getH() + elapsed*300) if (self.keyMap["right"]!=0): self.ralph.setH(self.ralph.getH() - elapsed*300) if (self.keyMap["forward"]!=0): self.ralph.setY(self.ralph, -(elapsed*25)) # If ralph is moving, loop the run animation. # If he is standing still, stop the animation. if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0): if self.isMoving is False: self.ralph.loop("run") self.isMoving = True else: if self.isMoving: self.ralph.stop() self.ralph.pose("walk",5) self.isMoving = False # If the camera is too far from ralph, move it closer. # If the camera is too close to ralph, move it farther. camvec = self.ralph.getPos() - base.camera.getPos() camvec.setZ(0) camdist = camvec.length() camvec.normalize() if (camdist > 10.0): base.camera.setPos(base.camera.getPos() + camvec*(camdist-10)) camdist = 10.0 if (camdist < 5.0): base.camera.setPos(base.camera.getPos() - camvec*(5-camdist)) camdist = 5.0 # Now check for collisions. self.cTrav.traverse(render) # Adjust ralph's Z coordinate. If ralph's ray hit terrain, # update his Z. If it hit anything else, or didn't hit anything, put # him back where he was last frame. entries = [] for i in range(self.ralphGroundHandler.getNumEntries()): entry = self.ralphGroundHandler.getEntry(i) entries.append(entry) entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(), x.getSurfacePoint(render).getZ())) if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"): self.ralph.setZ(entries[0].getSurfacePoint(render).getZ()) else: self.ralph.setPos(startpos) # Keep the camera at one foot above the terrain, # or two feet above ralph, whichever is greater. entries = [] for i in range(self.camGroundHandler.getNumEntries()): entry = self.camGroundHandler.getEntry(i) entries.append(entry) entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(), x.getSurfacePoint(render).getZ())) if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"): base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+1.0) if (base.camera.getZ() < self.ralph.getZ() + 2.0): base.camera.setZ(self.ralph.getZ() + 2.0) # The camera should look in ralph's direction, # but it should also try to stay horizontal, so look at # a floater which hovers above ralph's head. self.floater.setPos(self.ralph.getPos()) self.floater.setZ(self.ralph.getZ() + 2.0) base.camera.lookAt(self.floater) return Task.cont
def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText(text="Panda3D: Tutorial - Collision Detection", style=1, fg=(1,1,1,1), pos=(0.7,-0.95), scale = .07, font = font) self.instructions = OnscreenText(text="Mouse pointer tilts the board", pos = (-1.3, .95), fg=(1,1,1,1), font = font, align = TextNode.ALeft, scale = .05) self.accept("escape", sys.exit) #Escape quits base.disableMouse() #Disable mouse-based camera control camera.setPosHpr(0, 0, 25, 0, -90, 0) #Place the camera #Load the maze and place it in the scene self.maze = loader.loadModel("models/samples/ball_in_maze/maze") self.maze.reparentTo(render) #Most times, you want collisions to be tested against invisible geometry #rather than every polygon. This is because testing against every polygon #in the scene is usually too slow. You can have simplified or approximate #geometry for the solids and still get good results. # #Sometimes you'll want to create and position your own collision solids in #code, but it's often easier to have them built automatically. This can be #done by adding special tags into an egg file. Check maze.egg and ball.egg #and look for lines starting with <Collide>. The part is brackets tells #Panda exactly what to do. Polyset means to use the polygons in that group #as solids, while Sphere tells panda to make a collision sphere around them #Keep means to keep the polygons in the group as visable geometry (good #for the ball, not for the triggers), and descend means to make sure that #the settings are applied to any subgroups. # #Once we have the collision tags in the models, we can get to them using #NodePath's find command #Find the collision node named wall_collide self.walls = self.maze.find("**/wall_collide") #Collision objects are sorted using BitMasks. BitMasks are ordinary numbers #with extra methods for working with them as binary bits. Every collision #solid has both a from mask and an into mask. Before Panda tests two #objects, it checks to make sure that the from and into collision masks #have at least one bit in common. That way things that shouldn't interact #won't. Normal model nodes have collision masks as well. By default they #are set to bit 20. If you want to collide against actual visable polygons, #set a from collide mask to include bit 20 # #For this example, we will make everything we want the ball to collide with #include bit 0 self.walls.node().setIntoCollideMask(BitMask32.bit(0)) #CollisionNodes are usually invisible but can be shown. Uncomment the next #line to see the collision walls #self.walls.show() #We will now find the triggers for the holes and set their masks to 0 as #well. We also set their names to make them easier to identify during #collisions self.loseTriggers = [] for i in range(6): trigger = self.maze.find("**/hole_collide" + str(i)) trigger.node().setIntoCollideMask(BitMask32.bit(0)) trigger.node().setName("loseTrigger") self.loseTriggers.append(trigger) #Uncomment this line to see the triggers #trigger.show() #Ground_collide is a single polygon on the same plane as the ground in the #maze. We will use a ray to collide with it so that we will know exactly #what height to put the ball at every frame. Since this is not something #that we want the ball itself to collide with, it has a different #bitmask. self.mazeGround = self.maze.find("**/ground_collide") self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1)) #Load the ball and attach it to the scene #It is on a root dummy node so that we can rotate the ball itself without #rotating the ray that will be attached to it self.ballRoot = render.attachNewNode("ballRoot") self.ball = loader.loadModel("models/samples/ball_in_maze/ball") self.ball.reparentTo(self.ballRoot) #Find the collison sphere for the ball which was created in the egg file #Notice that it has a from collision mask of bit 0, and an into collison #mask of no bits. This means that the ball can only cause collisions, not #be collided into self.ballSphere = self.ball.find("**/ball") self.ballSphere.node().setFromCollideMask(BitMask32.bit(0)) self.ballSphere.node().setIntoCollideMask(BitMask32.allOff()) #No we create a ray to start above the ball and cast down. This is to #Determine the height the ball should be at and the angle the floor is #tilting. We could have used the sphere around the ball itself, but it #would not be as reliable self.ballGroundRay = CollisionRay() #Create the ray self.ballGroundRay.setOrigin(0,0,10) #Set its origin self.ballGroundRay.setDirection(0,0,-1) #And its direction #Collision solids go in CollisionNode self.ballGroundCol = CollisionNode('groundRay') #Create and name the node self.ballGroundCol.addSolid(self.ballGroundRay) #Add the ray self.ballGroundCol.setFromCollideMask(BitMask32.bit(1)) #Set its bitmasks self.ballGroundCol.setIntoCollideMask(BitMask32.allOff()) #Attach the node to the ballRoot so that the ray is relative to the ball #(it will always be 10 feet over the ball and point down) self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol) #Uncomment this line to see the ray #self.ballGroundColNp.show() #Finally, we create a CollisionTraverser. CollisionTraversers are what #do the job of calculating collisions self.cTrav = CollisionTraverser() #Collision traverservs tell collision handlers about collisions, and then #the handler decides what to do with the information. We are using a #CollisionHandlerQueue, which simply creates a list of all of the #collisions in a given pass. There are more sophisticated handlers like #one that sends events and another that tries to keep collided objects #apart, but the results are often better with a simple queue self.cHandler = CollisionHandlerQueue() #Now we add the collision nodes that can create a collision to the #traverser. The traverser will compare these to all others nodes in the #scene. There is a limit of 32 CollisionNodes per traverser #We add the collider, and the handler to use as a pair self.cTrav.addCollider(self.ballSphere, self.cHandler) self.cTrav.addCollider(self.ballGroundColNp, self.cHandler) #Collision traversers have a built in tool to help visualize collisions. #Uncomment the next line to see it. #self.cTrav.showCollisions(render) #This section deals with lighting for the ball. Only the ball was lit #because the maze has static lighting pregenerated by the modeler lAttrib = LightAttrib.makeAllOff() ambientLight = AmbientLight( "ambientLight" ) ambientLight.setColor( Vec4(.55, .55, .55, 1) ) lAttrib = lAttrib.addLight( ambientLight ) directionalLight = DirectionalLight( "directionalLight" ) directionalLight.setDirection( Vec3( 0, 0, -1 ) ) directionalLight.setColor( Vec4( 0.375, 0.375, 0.375, 1 ) ) directionalLight.setSpecularColor(Vec4(1,1,1,1)) lAttrib = lAttrib.addLight( directionalLight ) self.ballRoot.node().setAttrib( lAttrib ) #This section deals with adding a specular highlight to the ball to make #it look shiny m = Material() m.setSpecular(Vec4(1,1,1,1)) m.setShininess(96) self.ball.setMaterial(m, 1) #Finally, we call start for more initialization self.start()
class World(DirectObject): def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText( text="Panda3D: Tutorial - Collision Detection", style=1, fg=(1, 1, 1, 1), pos=(0.7, -0.95), scale=.07, font=font) self.instructions = OnscreenText(text="Mouse pointer tilts the board", pos=(-1.3, .95), fg=(1, 1, 1, 1), font=font, align=TextNode.ALeft, scale=.05) self.accept("escape", sys.exit) #Escape quits base.disableMouse() #Disable mouse-based camera control camera.setPosHpr(0, 0, 25, 0, -90, 0) #Place the camera #Load the maze and place it in the scene self.maze = loader.loadModel("models/samples/ball_in_maze/maze") self.maze.reparentTo(render) #Most times, you want collisions to be tested against invisible geometry #rather than every polygon. This is because testing against every polygon #in the scene is usually too slow. You can have simplified or approximate #geometry for the solids and still get good results. # #Sometimes you'll want to create and position your own collision solids in #code, but it's often easier to have them built automatically. This can be #done by adding special tags into an egg file. Check maze.egg and ball.egg #and look for lines starting with <Collide>. The part is brackets tells #Panda exactly what to do. Polyset means to use the polygons in that group #as solids, while Sphere tells panda to make a collision sphere around them #Keep means to keep the polygons in the group as visable geometry (good #for the ball, not for the triggers), and descend means to make sure that #the settings are applied to any subgroups. # #Once we have the collision tags in the models, we can get to them using #NodePath's find command #Find the collision node named wall_collide self.walls = self.maze.find("**/wall_collide") #Collision objects are sorted using BitMasks. BitMasks are ordinary numbers #with extra methods for working with them as binary bits. Every collision #solid has both a from mask and an into mask. Before Panda tests two #objects, it checks to make sure that the from and into collision masks #have at least one bit in common. That way things that shouldn't interact #won't. Normal model nodes have collision masks as well. By default they #are set to bit 20. If you want to collide against actual visable polygons, #set a from collide mask to include bit 20 # #For this example, we will make everything we want the ball to collide with #include bit 0 self.walls.node().setIntoCollideMask(BitMask32.bit(0)) #CollisionNodes are usually invisible but can be shown. Uncomment the next #line to see the collision walls #self.walls.show() #We will now find the triggers for the holes and set their masks to 0 as #well. We also set their names to make them easier to identify during #collisions self.loseTriggers = [] for i in range(6): trigger = self.maze.find("**/hole_collide" + str(i)) trigger.node().setIntoCollideMask(BitMask32.bit(0)) trigger.node().setName("loseTrigger") self.loseTriggers.append(trigger) #Uncomment this line to see the triggers #trigger.show() #Ground_collide is a single polygon on the same plane as the ground in the #maze. We will use a ray to collide with it so that we will know exactly #what height to put the ball at every frame. Since this is not something #that we want the ball itself to collide with, it has a different #bitmask. self.mazeGround = self.maze.find("**/ground_collide") self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1)) #Load the ball and attach it to the scene #It is on a root dummy node so that we can rotate the ball itself without #rotating the ray that will be attached to it self.ballRoot = render.attachNewNode("ballRoot") self.ball = loader.loadModel("models/samples/ball_in_maze/ball") self.ball.reparentTo(self.ballRoot) #Find the collison sphere for the ball which was created in the egg file #Notice that it has a from collision mask of bit 0, and an into collison #mask of no bits. This means that the ball can only cause collisions, not #be collided into self.ballSphere = self.ball.find("**/ball") self.ballSphere.node().setFromCollideMask(BitMask32.bit(0)) self.ballSphere.node().setIntoCollideMask(BitMask32.allOff()) #No we create a ray to start above the ball and cast down. This is to #Determine the height the ball should be at and the angle the floor is #tilting. We could have used the sphere around the ball itself, but it #would not be as reliable self.ballGroundRay = CollisionRay() #Create the ray self.ballGroundRay.setOrigin(0, 0, 10) #Set its origin self.ballGroundRay.setDirection(0, 0, -1) #And its direction #Collision solids go in CollisionNode self.ballGroundCol = CollisionNode( 'groundRay') #Create and name the node self.ballGroundCol.addSolid(self.ballGroundRay) #Add the ray self.ballGroundCol.setFromCollideMask( BitMask32.bit(1)) #Set its bitmasks self.ballGroundCol.setIntoCollideMask(BitMask32.allOff()) #Attach the node to the ballRoot so that the ray is relative to the ball #(it will always be 10 feet over the ball and point down) self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol) #Uncomment this line to see the ray #self.ballGroundColNp.show() #Finally, we create a CollisionTraverser. CollisionTraversers are what #do the job of calculating collisions self.cTrav = CollisionTraverser() #Collision traverservs tell collision handlers about collisions, and then #the handler decides what to do with the information. We are using a #CollisionHandlerQueue, which simply creates a list of all of the #collisions in a given pass. There are more sophisticated handlers like #one that sends events and another that tries to keep collided objects #apart, but the results are often better with a simple queue self.cHandler = CollisionHandlerQueue() #Now we add the collision nodes that can create a collision to the #traverser. The traverser will compare these to all others nodes in the #scene. There is a limit of 32 CollisionNodes per traverser #We add the collider, and the handler to use as a pair self.cTrav.addCollider(self.ballSphere, self.cHandler) self.cTrav.addCollider(self.ballGroundColNp, self.cHandler) #Collision traversers have a built in tool to help visualize collisions. #Uncomment the next line to see it. #self.cTrav.showCollisions(render) #This section deals with lighting for the ball. Only the ball was lit #because the maze has static lighting pregenerated by the modeler lAttrib = LightAttrib.makeAllOff() ambientLight = AmbientLight("ambientLight") ambientLight.setColor(Vec4(.55, .55, .55, 1)) lAttrib = lAttrib.addLight(ambientLight) directionalLight = DirectionalLight("directionalLight") directionalLight.setDirection(Vec3(0, 0, -1)) directionalLight.setColor(Vec4(0.375, 0.375, 0.375, 1)) directionalLight.setSpecularColor(Vec4(1, 1, 1, 1)) lAttrib = lAttrib.addLight(directionalLight) self.ballRoot.node().setAttrib(lAttrib) #This section deals with adding a specular highlight to the ball to make #it look shiny m = Material() m.setSpecular(Vec4(1, 1, 1, 1)) m.setShininess(96) self.ball.setMaterial(m, 1) #Finally, we call start for more initialization self.start() def start(self): #The maze model also has a locator in it for where to start the ball #To access it we use the find command startPos = self.maze.find("**/start").getPos() self.ballRoot.setPos(startPos) #Set the ball in the starting position self.ballV = Vec3(0, 0, 0) #Initial velocity is 0 self.accelV = Vec3(0, 0, 0) #Initial acceleration is 0 #For a traverser to actually do collisions, you need to call #traverser.traverse() on a part of the scene. Fortunatly, base has a #task that does this for the entire scene once a frame. This sets up our #traverser as the one to be called automatically base.cTrav = self.cTrav #Create the movement task, but first make sure it is not already running taskMgr.remove("rollTask") self.mainLoop = taskMgr.add(self.rollTask, "rollTask") self.mainLoop.last = 0 #This function handles the collision between the ray and the ground #Information about the interaction is passed in colEntry def groundCollideHandler(self, colEntry): #Set the ball to the appropriate Z value for it to be exactly on the ground newZ = colEntry.getSurfacePoint(render).getZ() self.ballRoot.setZ(newZ + .4) #Find the acceleration direction. First the surface normal is crossed with #the up vector to get a vector perpendicular to the slope norm = colEntry.getSurfaceNormal(render) accelSide = norm.cross(UP) #Then that vector is crossed with the surface normal to get a vector that #points down the slope. By getting the acceleration in 3D like this rather #than in 2D, we reduce the amount of error per-frame, reducing jitter self.accelV = norm.cross(accelSide) #This function handles the collision between the ball and a wall def wallCollideHandler(self, colEntry): #First we calculate some numbers we need to do a reflection norm = colEntry.getSurfaceNormal(render) * -1 #The normal of the wall curSpeed = self.ballV.length() #The current speed inVec = self.ballV / curSpeed #The direction of travel velAngle = norm.dot(inVec) #Angle of incidance hitDir = colEntry.getSurfacePoint(render) - self.ballRoot.getPos() hitDir.normalize() hitAngle = norm.dot(hitDir) #The angle between the ball and the normal #Ignore the collision if the ball is either moving away from the wall #already (so that we don't accidentally send it back into the wall) #and ignore it if the collision isn't dead-on (to avoid getting caught on #corners) if velAngle > 0 and hitAngle > .995: #Standard reflection equation reflectVec = (norm * norm.dot(inVec * -1) * 2) + inVec #This makes the velocity half of what it was if the hit was dead-on #and nearly exactly what it was if this is a glancing blow self.ballV = reflectVec * (curSpeed * (((1 - velAngle) * .5) + .5)) #Since we have a collision, the ball is already a little bit buried in #the wall. This calculates a vector needed to move it so that it is #exactly touching the wall disp = (colEntry.getSurfacePoint(render) - colEntry.getInteriorPoint(render)) newPos = self.ballRoot.getPos() + disp self.ballRoot.setPos(newPos) #This is the task that deals with making everything interactive def rollTask(self, task): #Standard technique for finding the amount of time since the last frame dt = task.time - task.last task.last = task.time #If dt is large, then there has been a #hiccup that could cause the ball #to leave the field if this functions runs, so ignore the frame if dt > .2: return Task.cont #The collision handler collects the collisions. We dispatch which function #to handle the collision based on the name of what was collided into for i in range(self.cHandler.getNumEntries()): entry = self.cHandler.getEntry(i) name = entry.getIntoNode().getName() if name == "wall_collide": self.wallCollideHandler(entry) elif name == "ground_collide": self.groundCollideHandler(entry) elif name == "loseTrigger": self.loseGame(entry) #Read the mouse position and tilt the maze accordingly if base.mouseWatcherNode.hasMouse(): mpos = base.mouseWatcherNode.getMouse() #get the mouse position self.maze.setP(mpos.getY() * -10) self.maze.setR(mpos.getX() * 10) #Finally, we move the ball #Update the velocity based on acceleration self.ballV += self.accelV * dt * ACCEL #Clamp the velocity to the maximum speed if self.ballV.lengthSquared() > MAX_SPEED_SQ: self.ballV.normalize() self.ballV *= MAX_SPEED #Update the position based on the velocity self.ballRoot.setPos(self.ballRoot.getPos() + (self.ballV * dt)) #This block of code rotates the ball. It uses something called a quaternion #to rotate the ball around an arbitrary axis. That axis perpendicular to #the balls rotation, and the amount has to do with the size of the ball #This is multiplied on the previous rotation to incrimentally turn it. prevRot = LRotationf(self.ball.getQuat()) axis = UP.cross(self.ballV) newRot = LRotationf(axis, 45.5 * dt * self.ballV.length()) self.ball.setQuat(prevRot * newRot) return Task.cont #Continue the task indefinitely #If the ball hits a hole trigger, then it should fall in the hole. #This is faked rather than dealing with the actual physics of it. def loseGame(self, entry): #The triggers are set up so that the center of the ball should move to the #collision point to be in the hole toPos = entry.getInteriorPoint(render) taskMgr.remove('rollTask') #Stop the maze task #Move the ball into the hole over a short sequence of time. Then wait a #second and call start to reset the game Sequence( Parallel( LerpFunc(self.ballRoot.setX, fromData=self.ballRoot.getX(), toData=toPos.getX(), duration=.1), LerpFunc(self.ballRoot.setY, fromData=self.ballRoot.getY(), toData=toPos.getY(), duration=.1), LerpFunc(self.ballRoot.setZ, fromData=self.ballRoot.getZ(), toData=self.ballRoot.getZ() - .9, duration=.2)), Wait(1), Func(self.start)).start()
class World(DirectObject): def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText(text="Panda3D: Tutorial - Collision Detection", style=1, fg=(1,1,1,1), pos=(0.7,-0.95), scale = .07, font = font) self.instructions = OnscreenText(text="Mouse pointer tilts the board", pos = (-1.3, .95), fg=(1,1,1,1), font = font, align = TextNode.ALeft, scale = .05) self.accept("escape", sys.exit) #Escape quits base.disableMouse() #Disable mouse-based camera control camera.setPosHpr(0, 0, 25, 0, -90, 0) #Place the camera #Load the maze and place it in the scene self.maze = loader.loadModel("models/samples/ball_in_maze/maze") self.maze.reparentTo(render) #Most times, you want collisions to be tested against invisible geometry #rather than every polygon. This is because testing against every polygon #in the scene is usually too slow. You can have simplified or approximate #geometry for the solids and still get good results. # #Sometimes you'll want to create and position your own collision solids in #code, but it's often easier to have them built automatically. This can be #done by adding special tags into an egg file. Check maze.egg and ball.egg #and look for lines starting with <Collide>. The part is brackets tells #Panda exactly what to do. Polyset means to use the polygons in that group #as solids, while Sphere tells panda to make a collision sphere around them #Keep means to keep the polygons in the group as visable geometry (good #for the ball, not for the triggers), and descend means to make sure that #the settings are applied to any subgroups. # #Once we have the collision tags in the models, we can get to them using #NodePath's find command #Find the collision node named wall_collide self.walls = self.maze.find("**/wall_collide") #Collision objects are sorted using BitMasks. BitMasks are ordinary numbers #with extra methods for working with them as binary bits. Every collision #solid has both a from mask and an into mask. Before Panda tests two #objects, it checks to make sure that the from and into collision masks #have at least one bit in common. That way things that shouldn't interact #won't. Normal model nodes have collision masks as well. By default they #are set to bit 20. If you want to collide against actual visable polygons, #set a from collide mask to include bit 20 # #For this example, we will make everything we want the ball to collide with #include bit 0 self.walls.node().setIntoCollideMask(BitMask32.bit(0)) #CollisionNodes are usually invisible but can be shown. Uncomment the next #line to see the collision walls #self.walls.show() #We will now find the triggers for the holes and set their masks to 0 as #well. We also set their names to make them easier to identify during #collisions self.loseTriggers = [] for i in range(6): trigger = self.maze.find("**/hole_collide" + str(i)) trigger.node().setIntoCollideMask(BitMask32.bit(0)) trigger.node().setName("loseTrigger") self.loseTriggers.append(trigger) #Uncomment this line to see the triggers #trigger.show() #Ground_collide is a single polygon on the same plane as the ground in the #maze. We will use a ray to collide with it so that we will know exactly #what height to put the ball at every frame. Since this is not something #that we want the ball itself to collide with, it has a different #bitmask. self.mazeGround = self.maze.find("**/ground_collide") self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1)) #Load the ball and attach it to the scene #It is on a root dummy node so that we can rotate the ball itself without #rotating the ray that will be attached to it self.ballRoot = render.attachNewNode("ballRoot") self.ball = loader.loadModel("models/samples/ball_in_maze/ball") self.ball.reparentTo(self.ballRoot) #Find the collison sphere for the ball which was created in the egg file #Notice that it has a from collision mask of bit 0, and an into collison #mask of no bits. This means that the ball can only cause collisions, not #be collided into self.ballSphere = self.ball.find("**/ball") self.ballSphere.node().setFromCollideMask(BitMask32.bit(0)) self.ballSphere.node().setIntoCollideMask(BitMask32.allOff()) #No we create a ray to start above the ball and cast down. This is to #Determine the height the ball should be at and the angle the floor is #tilting. We could have used the sphere around the ball itself, but it #would not be as reliable self.ballGroundRay = CollisionRay() #Create the ray self.ballGroundRay.setOrigin(0,0,10) #Set its origin self.ballGroundRay.setDirection(0,0,-1) #And its direction #Collision solids go in CollisionNode self.ballGroundCol = CollisionNode('groundRay') #Create and name the node self.ballGroundCol.addSolid(self.ballGroundRay) #Add the ray self.ballGroundCol.setFromCollideMask(BitMask32.bit(1)) #Set its bitmasks self.ballGroundCol.setIntoCollideMask(BitMask32.allOff()) #Attach the node to the ballRoot so that the ray is relative to the ball #(it will always be 10 feet over the ball and point down) self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol) #Uncomment this line to see the ray #self.ballGroundColNp.show() #Finally, we create a CollisionTraverser. CollisionTraversers are what #do the job of calculating collisions self.cTrav = CollisionTraverser() #Collision traverservs tell collision handlers about collisions, and then #the handler decides what to do with the information. We are using a #CollisionHandlerQueue, which simply creates a list of all of the #collisions in a given pass. There are more sophisticated handlers like #one that sends events and another that tries to keep collided objects #apart, but the results are often better with a simple queue self.cHandler = CollisionHandlerQueue() #Now we add the collision nodes that can create a collision to the #traverser. The traverser will compare these to all others nodes in the #scene. There is a limit of 32 CollisionNodes per traverser #We add the collider, and the handler to use as a pair self.cTrav.addCollider(self.ballSphere, self.cHandler) self.cTrav.addCollider(self.ballGroundColNp, self.cHandler) #Collision traversers have a built in tool to help visualize collisions. #Uncomment the next line to see it. #self.cTrav.showCollisions(render) #This section deals with lighting for the ball. Only the ball was lit #because the maze has static lighting pregenerated by the modeler lAttrib = LightAttrib.makeAllOff() ambientLight = AmbientLight( "ambientLight" ) ambientLight.setColor( Vec4(.55, .55, .55, 1) ) lAttrib = lAttrib.addLight( ambientLight ) directionalLight = DirectionalLight( "directionalLight" ) directionalLight.setDirection( Vec3( 0, 0, -1 ) ) directionalLight.setColor( Vec4( 0.375, 0.375, 0.375, 1 ) ) directionalLight.setSpecularColor(Vec4(1,1,1,1)) lAttrib = lAttrib.addLight( directionalLight ) self.ballRoot.node().setAttrib( lAttrib ) #This section deals with adding a specular highlight to the ball to make #it look shiny m = Material() m.setSpecular(Vec4(1,1,1,1)) m.setShininess(96) self.ball.setMaterial(m, 1) #Finally, we call start for more initialization self.start() def start(self): #The maze model also has a locator in it for where to start the ball #To access it we use the find command startPos = self.maze.find("**/start").getPos() self.ballRoot.setPos(startPos) #Set the ball in the starting position self.ballV = Vec3(0,0,0) #Initial velocity is 0 self.accelV = Vec3(0,0,0) #Initial acceleration is 0 #For a traverser to actually do collisions, you need to call #traverser.traverse() on a part of the scene. Fortunatly, base has a #task that does this for the entire scene once a frame. This sets up our #traverser as the one to be called automatically base.cTrav = self.cTrav #Create the movement task, but first make sure it is not already running taskMgr.remove("rollTask") self.mainLoop = taskMgr.add(self.rollTask, "rollTask") self.mainLoop.last = 0 #This function handles the collision between the ray and the ground #Information about the interaction is passed in colEntry def groundCollideHandler(self, colEntry): #Set the ball to the appropriate Z value for it to be exactly on the ground newZ = colEntry.getSurfacePoint(render).getZ() self.ballRoot.setZ(newZ+.4) #Find the acceleration direction. First the surface normal is crossed with #the up vector to get a vector perpendicular to the slope norm = colEntry.getSurfaceNormal(render) accelSide = norm.cross(UP) #Then that vector is crossed with the surface normal to get a vector that #points down the slope. By getting the acceleration in 3D like this rather #than in 2D, we reduce the amount of error per-frame, reducing jitter self.accelV = norm.cross(accelSide) #This function handles the collision between the ball and a wall def wallCollideHandler(self, colEntry): #First we calculate some numbers we need to do a reflection norm = colEntry.getSurfaceNormal(render) * -1 #The normal of the wall curSpeed = self.ballV.length() #The current speed inVec = self.ballV / curSpeed #The direction of travel velAngle = norm.dot(inVec) #Angle of incidance hitDir = colEntry.getSurfacePoint(render) - self.ballRoot.getPos() hitDir.normalize() hitAngle = norm.dot(hitDir) #The angle between the ball and the normal #Ignore the collision if the ball is either moving away from the wall #already (so that we don't accidentally send it back into the wall) #and ignore it if the collision isn't dead-on (to avoid getting caught on #corners) if velAngle > 0 and hitAngle > .995: #Standard reflection equation reflectVec = (norm * norm.dot(inVec * -1) * 2) + inVec #This makes the velocity half of what it was if the hit was dead-on #and nearly exactly what it was if this is a glancing blow self.ballV = reflectVec * (curSpeed * (((1-velAngle)*.5)+.5)) #Since we have a collision, the ball is already a little bit buried in #the wall. This calculates a vector needed to move it so that it is #exactly touching the wall disp = (colEntry.getSurfacePoint(render) - colEntry.getInteriorPoint(render)) newPos = self.ballRoot.getPos() + disp self.ballRoot.setPos(newPos) #This is the task that deals with making everything interactive def rollTask(self, task): #Standard technique for finding the amount of time since the last frame dt = task.time - task.last task.last = task.time #If dt is large, then there has been a #hiccup that could cause the ball #to leave the field if this functions runs, so ignore the frame if dt > .2: return Task.cont #The collision handler collects the collisions. We dispatch which function #to handle the collision based on the name of what was collided into for i in range(self.cHandler.getNumEntries()): entry = self.cHandler.getEntry(i) name = entry.getIntoNode().getName() if name == "wall_collide": self.wallCollideHandler(entry) elif name == "ground_collide": self.groundCollideHandler(entry) elif name == "loseTrigger": self.loseGame(entry) #Read the mouse position and tilt the maze accordingly if base.mouseWatcherNode.hasMouse(): mpos = base.mouseWatcherNode.getMouse() #get the mouse position self.maze.setP(mpos.getY() * -10) self.maze.setR(mpos.getX() * 10) #Finally, we move the ball #Update the velocity based on acceleration self.ballV += self.accelV * dt * ACCEL #Clamp the velocity to the maximum speed if self.ballV.lengthSquared() > MAX_SPEED_SQ: self.ballV.normalize() self.ballV *= MAX_SPEED #Update the position based on the velocity self.ballRoot.setPos(self.ballRoot.getPos() + (self.ballV * dt)) #This block of code rotates the ball. It uses something called a quaternion #to rotate the ball around an arbitrary axis. That axis perpendicular to #the balls rotation, and the amount has to do with the size of the ball #This is multiplied on the previous rotation to incrimentally turn it. prevRot = LRotationf(self.ball.getQuat()) axis = UP.cross(self.ballV) newRot = LRotationf(axis, 45.5 * dt * self.ballV.length()) self.ball.setQuat(prevRot * newRot) return Task.cont #Continue the task indefinitely #If the ball hits a hole trigger, then it should fall in the hole. #This is faked rather than dealing with the actual physics of it. def loseGame(self, entry): #The triggers are set up so that the center of the ball should move to the #collision point to be in the hole toPos = entry.getInteriorPoint(render) taskMgr.remove('rollTask') #Stop the maze task #Move the ball into the hole over a short sequence of time. Then wait a #second and call start to reset the game Sequence( Parallel( LerpFunc(self.ballRoot.setX, fromData = self.ballRoot.getX(), toData = toPos.getX(), duration = .1), LerpFunc(self.ballRoot.setY, fromData = self.ballRoot.getY(), toData = toPos.getY(), duration = .1), LerpFunc(self.ballRoot.setZ, fromData = self.ballRoot.getZ(), toData = self.ballRoot.getZ() - .9, duration = .2)), Wait(1), Func(self.start)).start()
def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText(text="Panda3D: Tutorial - Mouse Picking", style=1, fg=(1,1,1,1), font = font, pos=(0.8,-0.95), scale = .07) self.escapeEvent = OnscreenText( text="ESC: Quit", font = font, style=1, fg=(1,1,1,1), pos=(-1.3, 0.95), align=TextNode.ALeft, scale = .05) self.mouse1Event = OnscreenText( text="Left-click and drag: Pick up and drag piece", style=1, fg=(1,1,1,1), pos=(-1.3, 0.90), font = font, align=TextNode.ALeft, scale = .05) self.accept('escape', sys.exit) #Escape quits base.disableMouse() #Disble mouse camera control camera.setPosHpr(0, -13.75, 6, 0, -25, 0) #Set the camera self.setupLights() #Setup default lighting #Since we are using collision detection to do picking, we set it up like #any other collision detection system with a traverser and a handler self.picker = CollisionTraverser() #Make a traverser self.pq = CollisionHandlerQueue() #Make a handler #Make a collision node for our picker ray self.pickerNode = CollisionNode('mouseRay') #Attach that node to the camera since the ray will need to be positioned #relative to it self.pickerNP = camera.attachNewNode(self.pickerNode) #Everything to be picked will use bit 1. This way if we were doing other #collision we could seperate it self.pickerNode.setFromCollideMask(BitMask32.bit(1)) self.pickerRay = CollisionRay() #Make our ray self.pickerNode.addSolid(self.pickerRay) #Add it to the collision node #Register the ray as something that can cause collisions self.picker.addCollider(self.pickerNP, self.pq) #self.picker.showCollisions(render) #Now we create the chess board and its pieces #We will attach all of the squares to their own root. This way we can do the #collision pass just on the sqaures and save the time of checking the rest #of the scene self.squareRoot = render.attachNewNode("squareRoot") #For each square self.squares = [None for i in range(64)] self.pieces = [None for i in range(64)] for i in range(64): #Load, parent, color, and position the model (a single square polygon) self.squares[i] = loader.loadModel("models/samples/chessboard/square") self.squares[i].reparentTo(self.squareRoot) self.squares[i].setPos(SquarePos(i)) self.squares[i].setColor(SquareColor(i)) #Set the model itself to be collideable with the ray. If this model was #any more complex than a single polygon, you should set up a collision #sphere around it instead. But for single polygons this works fine. self.squares[i].find("**/polygon").node().setIntoCollideMask( BitMask32.bit(1)) #Set a tag on the square's node so we can look up what square this is #later during the collision pass self.squares[i].find("**/polygon").node().setTag('square', str(i)) #We will use this variable as a pointer to whatever piece is currently #in this square #The order of pieces on a chessboard from white's perspective. This list #contains the constructor functions for the piece classes defined below pieceOrder = (Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook) for i in range (8,16): #Load the white pawns self.pieces[i] = Pawn(i, WHITE) for i in range (48,56): #load the black pawns self.pieces[i] = Pawn(i, PIECEBLACK) for i in range(8): #Load the special pieces for the front row and color them white self.pieces[i] = pieceOrder[i](i, WHITE) #Load the special pieces for the back row and color them black self.pieces[i+56] = pieceOrder[i](i+56, PIECEBLACK) #This will represent the index of the currently highlited square self.hiSq = False #This wil represent the index of the square where currently dragged piece #was grabbed from self.dragging = False #Start the task that handles the picking self.mouseTask = taskMgr.add(self.mouseTask, 'mouseTask') self.accept("mouse1", self.grabPiece) #left-click grabs a piece self.accept("mouse1-up", self.releasePiece) #releasing places it
class World(DirectObject): def __init__(self): #This code puts the standard title and instruction text on screen self.title = OnscreenText(text="Panda3D: Tutorial - Mouse Picking", style=1, fg=(1,1,1,1), font = font, pos=(0.8,-0.95), scale = .07) self.escapeEvent = OnscreenText( text="ESC: Quit", font = font, style=1, fg=(1,1,1,1), pos=(-1.3, 0.95), align=TextNode.ALeft, scale = .05) self.mouse1Event = OnscreenText( text="Left-click and drag: Pick up and drag piece", style=1, fg=(1,1,1,1), pos=(-1.3, 0.90), font = font, align=TextNode.ALeft, scale = .05) self.accept('escape', sys.exit) #Escape quits base.disableMouse() #Disble mouse camera control camera.setPosHpr(0, -13.75, 6, 0, -25, 0) #Set the camera self.setupLights() #Setup default lighting #Since we are using collision detection to do picking, we set it up like #any other collision detection system with a traverser and a handler self.picker = CollisionTraverser() #Make a traverser self.pq = CollisionHandlerQueue() #Make a handler #Make a collision node for our picker ray self.pickerNode = CollisionNode('mouseRay') #Attach that node to the camera since the ray will need to be positioned #relative to it self.pickerNP = camera.attachNewNode(self.pickerNode) #Everything to be picked will use bit 1. This way if we were doing other #collision we could seperate it self.pickerNode.setFromCollideMask(BitMask32.bit(1)) self.pickerRay = CollisionRay() #Make our ray self.pickerNode.addSolid(self.pickerRay) #Add it to the collision node #Register the ray as something that can cause collisions self.picker.addCollider(self.pickerNP, self.pq) #self.picker.showCollisions(render) #Now we create the chess board and its pieces #We will attach all of the squares to their own root. This way we can do the #collision pass just on the sqaures and save the time of checking the rest #of the scene self.squareRoot = render.attachNewNode("squareRoot") #For each square self.squares = [None for i in range(64)] self.pieces = [None for i in range(64)] for i in range(64): #Load, parent, color, and position the model (a single square polygon) self.squares[i] = loader.loadModel("models/samples/chessboard/square") self.squares[i].reparentTo(self.squareRoot) self.squares[i].setPos(SquarePos(i)) self.squares[i].setColor(SquareColor(i)) #Set the model itself to be collideable with the ray. If this model was #any more complex than a single polygon, you should set up a collision #sphere around it instead. But for single polygons this works fine. self.squares[i].find("**/polygon").node().setIntoCollideMask( BitMask32.bit(1)) #Set a tag on the square's node so we can look up what square this is #later during the collision pass self.squares[i].find("**/polygon").node().setTag('square', str(i)) #We will use this variable as a pointer to whatever piece is currently #in this square #The order of pieces on a chessboard from white's perspective. This list #contains the constructor functions for the piece classes defined below pieceOrder = (Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook) for i in range (8,16): #Load the white pawns self.pieces[i] = Pawn(i, WHITE) for i in range (48,56): #load the black pawns self.pieces[i] = Pawn(i, PIECEBLACK) for i in range(8): #Load the special pieces for the front row and color them white self.pieces[i] = pieceOrder[i](i, WHITE) #Load the special pieces for the back row and color them black self.pieces[i+56] = pieceOrder[i](i+56, PIECEBLACK) #This will represent the index of the currently highlited square self.hiSq = False #This wil represent the index of the square where currently dragged piece #was grabbed from self.dragging = False #Start the task that handles the picking self.mouseTask = taskMgr.add(self.mouseTask, 'mouseTask') self.accept("mouse1", self.grabPiece) #left-click grabs a piece self.accept("mouse1-up", self.releasePiece) #releasing places it #This function swaps the positions of two pieces def swapPieces(self, fr, to): temp = self.pieces[fr] self.pieces[fr] = self.pieces[to] self.pieces[to] = temp if self.pieces[fr]: self.pieces[fr].square = fr self.pieces[fr].obj.setPos(SquarePos(fr)) if self.pieces[to]: self.pieces[to].square = to self.pieces[to].obj.setPos(SquarePos(to)) def mouseTask(self, task): #This task deals with the highlighting and dragging based on the mouse #First, clear the current highlight if self.hiSq is not False: self.squares[self.hiSq].setColor(SquareColor(self.hiSq)) self.hiSq = False #Check to see if we can access the mouse. We need it to do anything else if base.mouseWatcherNode.hasMouse(): #get the mouse position mpos = base.mouseWatcherNode.getMouse() #Set the position of the ray based on the mouse position self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY()) #If we are dragging something, set the position of the object #to be at the appropriate point over the plane of the board if self.dragging is not False: #Gets the point described by pickerRay.getOrigin(), which is relative to #camera, relative instead to render nearPoint = render.getRelativePoint(camera, self.pickerRay.getOrigin()) #Same thing with the direction of the ray nearVec = render.getRelativeVector(camera, self.pickerRay.getDirection()) self.pieces[self.dragging].obj.setPos( PointAtZ(.5, nearPoint, nearVec)) #Do the actual collision pass (Do it only on the squares for #efficiency purposes) self.picker.traverse(self.squareRoot) if self.pq.getNumEntries() > 0: #if we have hit something, sort the hits so that the closest #is first, and highlight that node self.pq.sortEntries() i = int(self.pq.getEntry(0).getIntoNode().getTag('square')) #Set the highlight on the picked square self.squares[i].setColor(HIGHLIGHT) self.hiSq = i return Task.cont def grabPiece(self): #If a square is highlighted and it has a piece, set it to dragging mode if (self.hiSq is not False and self.pieces[self.hiSq]): self.dragging = self.hiSq self.hiSq = False def releasePiece(self): #Letting go of a piece. If we are not on a square, return it to its original #position. Otherwise, swap it with the piece in the new square if self.dragging is not False: #Make sure we really are dragging something #We have let go of the piece, but we are not on a square if self.hiSq is False: self.pieces[self.dragging].obj.setPos( SquarePos(self.dragging)) else: #Otherwise, swap the pieces self.swapPieces(self.dragging, self.hiSq) #We are no longer dragging anything self.dragging = False def setupLights(self): #This function sets up some default lighting lAttrib = LightAttrib.makeAllOff() ambientLight = AmbientLight( "ambientLight" ) ambientLight.setColor( Vec4(.8, .8, .8, 1) ) lAttrib = lAttrib.addLight( ambientLight ) directionalLight = DirectionalLight( "directionalLight" ) directionalLight.setDirection( Vec3( 0, 45, -45 ) ) directionalLight.setColor( Vec4( 0.2, 0.2, 0.2, 1 ) ) lAttrib = lAttrib.addLight( directionalLight ) render.attachNewNode( directionalLight ) render.attachNewNode( ambientLight ) render.node().setAttrib( lAttrib )
def __init__(self): self.keyMap = { "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0 } base.win.setClearColor(Vec4(0, 0, 0, 1)) # Post the instructions self.title = addTitle( "Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)") self.inst1 = addInstructions(0.95, "[ESC]: Quit") self.inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left") self.inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right") self.inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward") self.inst6 = addInstructions(0.70, "[A]: Rotate Camera Left") self.inst7 = addInstructions(0.65, "[S]: Rotate Camera Right") # Set up the environment # # This environment model contains collision meshes. If you look # in the egg file, you will see the following: # # <Collide> { Polyset keep descend } # # This tag causes the following mesh to be converted to a collision # mesh -- a mesh which is optimized for collision, not rendering. # It also keeps the original mesh, so there are now two copies --- # one optimized for rendering, one for collisions. self.environ = loader.loadModel("models/samples/roaming_ralph/world") self.environ.reparentTo(render) self.environ.setPos(0, 0, 0) # Create the main character, Ralph ralphStartPos = self.environ.find("**/start_point").getPos() self.ralph = Actor( "models/samples/roaming_ralph/ralph", { "run": "models/samples/roaming_ralph/ralph_run", "walk": "models/samples/roaming_ralph/ralph_walk" }) self.ralph.reparentTo(render) self.ralph.setScale(.2) self.ralph.setPos(ralphStartPos) # Create a floater object. We use the "floater" as a temporary # variable in a variety of calculations. self.floater = NodePath(PandaNode("floater")) self.floater.reparentTo(render) # Accept the control keys for movement and rotation self.accept("escape", sys.exit) self.accept("arrow_left", self.setKey, ["left", 1]) self.accept("arrow_right", self.setKey, ["right", 1]) self.accept("arrow_up", self.setKey, ["forward", 1]) self.accept("a", self.setKey, ["cam-left", 1]) self.accept("s", self.setKey, ["cam-right", 1]) self.accept("arrow_left-up", self.setKey, ["left", 0]) self.accept("arrow_right-up", self.setKey, ["right", 0]) self.accept("arrow_up-up", self.setKey, ["forward", 0]) self.accept("a-up", self.setKey, ["cam-left", 0]) self.accept("s-up", self.setKey, ["cam-right", 0]) taskMgr.add(self.move, "moveTask") # Game state variables self.isMoving = False # Set up the camera base.disableMouse() base.camera.setPos(self.ralph.getX(), self.ralph.getY() + 10, 2) # We will detect the height of the terrain by creating a collision # ray and casting it downward toward the terrain. One ray will # start above ralph's head, and the other will start above the camera. # A ray may hit the terrain, or it may hit a rock or a tree. If it # hits the terrain, we can detect the height. If it hits anything # else, we rule that the move is illegal. self.cTrav = CollisionTraverser() self.ralphGroundRay = CollisionRay() self.ralphGroundRay.setOrigin(0, 0, 1000) self.ralphGroundRay.setDirection(0, 0, -1) self.ralphGroundCol = CollisionNode('ralphRay') self.ralphGroundCol.addSolid(self.ralphGroundRay) self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0)) self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff()) self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol) self.ralphGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler) self.camGroundRay = CollisionRay() self.camGroundRay.setOrigin(0, 0, 1000) self.camGroundRay.setDirection(0, 0, -1) self.camGroundCol = CollisionNode('camRay') self.camGroundCol.addSolid(self.camGroundRay) self.camGroundCol.setFromCollideMask(BitMask32.bit(0)) self.camGroundCol.setIntoCollideMask(BitMask32.allOff()) self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol) self.camGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler)
class World(DirectObject): def __init__(self): self.keyMap = { "left": 0, "right": 0, "forward": 0, "cam-left": 0, "cam-right": 0 } base.win.setClearColor(Vec4(0, 0, 0, 1)) # Post the instructions self.title = addTitle( "Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)") self.inst1 = addInstructions(0.95, "[ESC]: Quit") self.inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left") self.inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right") self.inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward") self.inst6 = addInstructions(0.70, "[A]: Rotate Camera Left") self.inst7 = addInstructions(0.65, "[S]: Rotate Camera Right") # Set up the environment # # This environment model contains collision meshes. If you look # in the egg file, you will see the following: # # <Collide> { Polyset keep descend } # # This tag causes the following mesh to be converted to a collision # mesh -- a mesh which is optimized for collision, not rendering. # It also keeps the original mesh, so there are now two copies --- # one optimized for rendering, one for collisions. self.environ = loader.loadModel("models/samples/roaming_ralph/world") self.environ.reparentTo(render) self.environ.setPos(0, 0, 0) # Create the main character, Ralph ralphStartPos = self.environ.find("**/start_point").getPos() self.ralph = Actor( "models/samples/roaming_ralph/ralph", { "run": "models/samples/roaming_ralph/ralph_run", "walk": "models/samples/roaming_ralph/ralph_walk" }) self.ralph.reparentTo(render) self.ralph.setScale(.2) self.ralph.setPos(ralphStartPos) # Create a floater object. We use the "floater" as a temporary # variable in a variety of calculations. self.floater = NodePath(PandaNode("floater")) self.floater.reparentTo(render) # Accept the control keys for movement and rotation self.accept("escape", sys.exit) self.accept("arrow_left", self.setKey, ["left", 1]) self.accept("arrow_right", self.setKey, ["right", 1]) self.accept("arrow_up", self.setKey, ["forward", 1]) self.accept("a", self.setKey, ["cam-left", 1]) self.accept("s", self.setKey, ["cam-right", 1]) self.accept("arrow_left-up", self.setKey, ["left", 0]) self.accept("arrow_right-up", self.setKey, ["right", 0]) self.accept("arrow_up-up", self.setKey, ["forward", 0]) self.accept("a-up", self.setKey, ["cam-left", 0]) self.accept("s-up", self.setKey, ["cam-right", 0]) taskMgr.add(self.move, "moveTask") # Game state variables self.isMoving = False # Set up the camera base.disableMouse() base.camera.setPos(self.ralph.getX(), self.ralph.getY() + 10, 2) # We will detect the height of the terrain by creating a collision # ray and casting it downward toward the terrain. One ray will # start above ralph's head, and the other will start above the camera. # A ray may hit the terrain, or it may hit a rock or a tree. If it # hits the terrain, we can detect the height. If it hits anything # else, we rule that the move is illegal. self.cTrav = CollisionTraverser() self.ralphGroundRay = CollisionRay() self.ralphGroundRay.setOrigin(0, 0, 1000) self.ralphGroundRay.setDirection(0, 0, -1) self.ralphGroundCol = CollisionNode('ralphRay') self.ralphGroundCol.addSolid(self.ralphGroundRay) self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0)) self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff()) self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol) self.ralphGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler) self.camGroundRay = CollisionRay() self.camGroundRay.setOrigin(0, 0, 1000) self.camGroundRay.setDirection(0, 0, -1) self.camGroundCol = CollisionNode('camRay') self.camGroundCol.addSolid(self.camGroundRay) self.camGroundCol.setFromCollideMask(BitMask32.bit(0)) self.camGroundCol.setIntoCollideMask(BitMask32.allOff()) self.camGroundColNp = base.camera.attachNewNode(self.camGroundCol) self.camGroundHandler = CollisionHandlerQueue() self.cTrav.addCollider(self.camGroundColNp, self.camGroundHandler) # Uncomment this line to see the collision rays #self.ralphGroundColNp.show() #self.camGroundColNp.show() #Uncomment this line to show a visual representation of the #collisions occuring #self.cTrav.showCollisions(render) #Records the state of the arrow keys def setKey(self, key, value): self.keyMap[key] = value # Accepts arrow keys to move either the player or the menu cursor, # Also deals with grid checking and collision detection def move(self, task): # Get the time elapsed since last frame. We need this # for framerate-independent movement. elapsed = globalClock.getDt() # If the camera-left key is pressed, move camera left. # If the camera-right key is pressed, move camera right. base.camera.lookAt(self.ralph) if (self.keyMap["cam-left"] != 0): base.camera.setX(base.camera, -(elapsed * 20)) if (self.keyMap["cam-right"] != 0): base.camera.setX(base.camera, +(elapsed * 20)) # save ralph's initial position so that we can restore it, # in case he falls off the map or runs into something. startpos = self.ralph.getPos() # If a move-key is pressed, move ralph in the specified direction. if (self.keyMap["left"] != 0): self.ralph.setH(self.ralph.getH() + elapsed * 300) if (self.keyMap["right"] != 0): self.ralph.setH(self.ralph.getH() - elapsed * 300) if (self.keyMap["forward"] != 0): self.ralph.setY(self.ralph, -(elapsed * 25)) # If ralph is moving, loop the run animation. # If he is standing still, stop the animation. if (self.keyMap["forward"] != 0) or (self.keyMap["left"] != 0) or (self.keyMap["right"] != 0): if self.isMoving is False: self.ralph.loop("run") self.isMoving = True else: if self.isMoving: self.ralph.stop() self.ralph.pose("walk", 5) self.isMoving = False # If the camera is too far from ralph, move it closer. # If the camera is too close to ralph, move it farther. camvec = self.ralph.getPos() - base.camera.getPos() camvec.setZ(0) camdist = camvec.length() camvec.normalize() if (camdist > 10.0): base.camera.setPos(base.camera.getPos() + camvec * (camdist - 10)) camdist = 10.0 if (camdist < 5.0): base.camera.setPos(base.camera.getPos() - camvec * (5 - camdist)) camdist = 5.0 # Now check for collisions. self.cTrav.traverse(render) # Adjust ralph's Z coordinate. If ralph's ray hit terrain, # update his Z. If it hit anything else, or didn't hit anything, put # him back where he was last frame. entries = [] for i in range(self.ralphGroundHandler.getNumEntries()): entry = self.ralphGroundHandler.getEntry(i) entries.append(entry) entries.sort(lambda x, y: cmp( y.getSurfacePoint(render).getZ(), x.getSurfacePoint(render).getZ())) if (len(entries) > 0) and (entries[0].getIntoNode().getName() == "terrain"): self.ralph.setZ(entries[0].getSurfacePoint(render).getZ()) else: self.ralph.setPos(startpos) # Keep the camera at one foot above the terrain, # or two feet above ralph, whichever is greater. entries = [] for i in range(self.camGroundHandler.getNumEntries()): entry = self.camGroundHandler.getEntry(i) entries.append(entry) entries.sort(lambda x, y: cmp( y.getSurfacePoint(render).getZ(), x.getSurfacePoint(render).getZ())) if (len(entries) > 0) and (entries[0].getIntoNode().getName() == "terrain"): base.camera.setZ(entries[0].getSurfacePoint(render).getZ() + 1.0) if (base.camera.getZ() < self.ralph.getZ() + 2.0): base.camera.setZ(self.ralph.getZ() + 2.0) # The camera should look in ralph's direction, # but it should also try to stay horizontal, so look at # a floater which hovers above ralph's head. self.floater.setPos(self.ralph.getPos()) self.floater.setZ(self.ralph.getZ() + 2.0) base.camera.lookAt(self.floater) return Task.cont