""" Copyright (c) 2007 Sean Hammond seanh@sdf.lonestar.org This file is part of PandaSteer. PandaSteer is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. PandaSteer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with PandaSteer; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ # Python imports import os,math,sys,random,imp # Panda3D imports from pandac import PandaModules as P from direct.gui import DirectGui as D from direct.showbase.DirectObject import DirectObject from direct.fsm import FSM from direct.actor.Actor import Actor from direct.task.Task import Task from pandac.PandaModules import loadPrcFileData loadPrcFileData("", "interpolate-frames 1") # Smooth (interpolated) animations # Custom imports from scene import makeTerrain from camera import Camera from vehicle import Vehicle from steerVec import SteerVec # FIXME: Add a loader class for loading character models, so that models can # be duplicated instead of reloaded if they are already loaded. # ynjh_jo: Doesn't Actor do this already? # Global CollisionTraverser, CollisionHandlers and collision bitmasks. base.cTrav = P.CollisionTraverser('Global CollisionTraverser of steer.py') obstacleHandler = P.CollisionHandlerQueue() collisionHandler = P.CollisionHandlerQueue() floorMASK = P.BitMask32.bit(1) obstacleMASK = P.BitMask32.bit(2) collisionMASK = P.BitMask32.bit(3) offMASK = P.BitMask32.allOff() class Marker: """A visual object that marks a target point being used by a Character.""" def __init__(self): """Initialise marker. At the moment simply loads jack and turns off his collisions, but you could easily make it do so much more.""" self.prime = loader.loadModel('jack') self.prime.reparentTo(render) self.prime.setScale(.4,.4,.4) self.prime.setCollideMask(offMASK) class SphereObstacle: """A CollisionSphere that registers itself with the global CollisionHandler for obstacles, so that Characters will treat it as an obstacle.""" def __init__(self,x=0,y=0,z=0,radius=1): """Initialise the CollisionSphere.""" cs = P.CollisionSphere(x,y,z,radius) self.cnp = render.attachNewNode(P.CollisionNode('cnode')) self.cnp.node().addSolid(cs) self.cnp.node().setIntoCollideMask(offMASK) self.cnp.node().setFromCollideMask(obstacleMASK) base.cTrav.addCollider(self.cnp,obstacleHandler) self.cnp.show() def destroy(self): """Remove the CollisionSphere from the global CollisionTraverser and detach the NodePath from the scene graph. After executing this method, the SphereObstacle is ready to be garbage-collected by Python once any remaining references to it in the user module are destroyed.""" base.cTrav.removeCollider(self.cnp) self.cnp.detachNode() class Character(FSM.FSM,Vehicle): """ An animated character with various steering behaviors. By enabling and disabling different steering behaviors Character can be used as a keyboard + mouse controlled player avatar, or can be controlled (for example) by a Finite State Machine and used to implement a non-player character. """ # FIXME: Think it might be clearer and safer if Character has a Vehicle # and has a FSM, instead of multiple-inheriting from Vehicle and FSM. def __init__(self, name='Ralph', model='models/ralph', run='models/ralph-run', walk='models/ralph-walk', pos=None, hprs=(180,0,0,1,1,1), # Ralph's Y is backward ): """Initialise the character. By default tries to load Panda3D's Ralph model: models/ralph, models/ralph-run and models/ralph-walk.""" FSM.FSM.__init__(self,'Character') Vehicle.__init__(self) self.name = name self._radius = 3 # Used when transitioning between animations. self.lastPose = 0 # The Character is controlled by a primary NodePath to which other # NodePath's for Actors and CollisionSolids are parented. if pos is None: pos = P.Point3(0,0,0) self.prime = P.NodePath('Character primary NodePath') self.prime.reparentTo(render) self.prime.setPos(pos) # Initialise the Character's Actor. self.actor = Actor(model, {"run":run, "walk":walk}) self.actor.setHprScale(*hprs) self.actor.reparentTo(self.prime) # Initialise the Character's CollisionSegment which is used with a # CollisionHandlerFloor to keep the Character's feet on the ground. self.segnp = self.prime.attachNewNode(P.CollisionNode('colNode')) self.segnp.node().addSolid(P.CollisionSegment(0, 0, 3, 0, 0, -5)) self.floorhandler = P.CollisionHandlerFloor() self.floorhandler.addCollider(self.segnp, self.prime) base.cTrav.addCollider(self.segnp, self.floorhandler) # Uncomment this line to show the Character's CollisionSegment: #self.segnp.show() # We do not want our CollisionSegment to collide with all renderable # geometry, but only with the renderable geometry of the terrain, so # we apply a collision mask here that is also applied to the terrain # later. self.segnp.node().setFromCollideMask(floorMASK) self.segnp.node().setIntoCollideMask(offMASK) # Initialise CollisionTube for detecting obstacles. x,y,z = pos.getX(),pos.getY(),pos.getZ()+3 vx,vy = self._velocity.getX(),self._velocity.getY() r = self._radius f = self.prime.getNetTransform().getMat().getRow3(1) s = 10 self.tube = P.CollisionTube(x,y,z,x+f.getX()*s,y+f.getY()*s,z,3) self.tubenp = self.prime.attachNewNode(P.CollisionNode('colNode')) self.tubenp.node().addSolid(self.tube) self.tubenp.node().setFromCollideMask(offMASK) self.tubenp.node().setIntoCollideMask(obstacleMASK) # Uncomment this line to show the CollisionTube #self.tubenp.show() # CollisionSphere for detecting collisions with other characters. self.sphere = P.CollisionSphere(x,y,z,self._radius) self.spherenp = self.prime.attachNewNode(P.CollisionNode('cnode')) self.spherenp.node().addSolid(self.sphere) self.spherenp.node().setCollideMask(collisionMASK) base.cTrav.addCollider(self.spherenp, collisionHandler) # Uncomment this line to show the CollisionSphere #self.spherenp.show() # Add a task for this Character to the global task manager. self.characterStep=taskMgr.add(self.step,"Character step task") self.prevtime = 0 self.marker = Marker() # Used to visually mark the Character's target. def step(self,task): """Update the character. Called every frame by Panda's global task manager.""" dt = task.time - self.prevtime # Check for collisions with obstacles. collision = None obstacleHandler.sortEntries() for i in range(obstacleHandler.getNumEntries()): entry = obstacleHandler.getEntry(i) if entry.getInto() == self.tube: collision = (entry.getSurfacePoint(render),entry.getSurfaceNormal(render)) # Update the underlying Vehicle object that steers the Character. Vehicle.step(self,dt,collision) # Fluidly update the Character's X and Y positions to match those of # the underlying Vehicle object. Leave the Character's Z-axis position # untouched (that is taken care of by CollisionHandlerFloor). self.prime.setFluidPos(self._pos.getX(),self._pos.getY(), self.prime.getZ()) # Update the orientation of the Character. Want to face in the # direction of the Vehicle's velocity in the X and Y dimensions, but # always face straight forward (not up or down) in the Z dimension. pr = self.prime.getP(),self.prime.getR() self.prime.lookAt((self._pos+self._velocity).getX(), (self._pos+self._velocity).getY(),0) self.prime.setHpr(self.prime.getH(),*pr) # Animate the Character's Actor. The Character automatically changes # between stand, run and walk animations and varies the playrate of the # animations depending on the steering force and velocity of the # Character's Vehicle. if self._steeringforce.length() > self._velocity.length(): greatest = self._steeringforce.length() else: greatest = self._velocity.length() if greatest < .05: if self.getCurrentOrNextState() != 'Stand': self.request('Stand') elif greatest < .25: self.actor.setPlayRate(greatest*4,"walk") if self.getCurrentOrNextState() != 'Walk': self.request('Walk') else: self.actor.setPlayRate(greatest*2,"run") if self.getCurrentOrNextState() != 'Run': self.request('Run') if self.reticle is not None: self.marker.prime.show() self.marker.prime.setPos(self.reticle.getX(),self.reticle.getY(),1) else: self.marker.prime.hide() self.prevtime = task.time return Task.cont def destroy(self): """Prepare this Character to be garbage-collected by Python: Remove all of the Character's nodes from the scene graph, remove its CollisionSolids from the global CollisionTraverser, clear its CollisionHandler, remove tasks, destroy Actor. After executing this method, any remaining references to the Character object can be destroyed by the user module, and the Character will be garbage-collected by Python. """ base.cTrav.removeCollider(self.segnp) base.cTrav.removeCollider(self.spherenp) self.floorhandler.clearColliders() self.floorhandler = None taskMgr.remove(self.characterStep) self.actor.delete() self.marker.prime.removeNode() self.prime.removeNode() # Methods for handling animations. def storeLastPose(self): currAnim=self.actor.getCurrentAnim() numFrames=self.actor.getNumFrames(currAnim) animFrame=self.actor.getCurrentFrame(currAnim) self.lastPose=float(animFrame)/float(numFrames) self.actor.stop(currAnim) def loopFromPose(self,animName): self.actor.pose(animName,frame=self.lastPose*self.actor.getNumFrames(animName)) self.actor.loop(animName,restart=0) # FSM State handlers. Called when transitioning to a new state. def enterRun(self): self.loopFromPose("run") def exitRun(self): self.storeLastPose() def enterWalk(self): self.loopFromPose("walk") def exitWalk(self): self.storeLastPose() def enterStand(self): standPoseFrame=6 # frame 6 (the most acceptable stand pose) numFrames=self.actor.getNumFrames("walk") lastFrame=self.lastPose*numFrames # "mirror" the frame to bring it closer to the most acceptable stand pose if lastFrame>.5*(numFrames-1): lastFrame=numFrames-1-lastFrame frameDiff=standPoseFrame-lastFrame # if already at stand pose, don't do anything if frameDiff==0: return # forward animation playback if frameDiff>=0: fromFrame=lastFrame toFrame=standPoseFrame else: # backward animation playback fromFrame=standPoseFrame toFrame=lastFrame playDir=2*frameDiff/numFrames self.actor.setPlayRate(playDir,"walk") self.actor.play("walk", fromFrame=fromFrame, toFrame=toFrame) from direct.gui.OnscreenText import OnscreenText class SteerTest(DirectObject): def __init__(self): """Initialise the test scene.""" # Show the framerate base.setFrameRateMeter(True) # Make some terrain and colour, scale and position it. tnp,tcollnp = makeTerrain(h=3) tnp.setColor(0.6,0.8,0.5,1) tnp.setPos(tnp,-50,-50,0) tnp.setScale(3) tcollnp.setCollideMask(floorMASK) # Setup a camera, position and orient it to look down at the terrain. base.disableMouse() self.camera = Camera(P.Vec3(-100,-100,100)) self.camera.lookAt(0,0,0) # Setup ambient and directional lights. self.alight = P.AmbientLight('alight') self.alight.setColor(P.VBase4(0.35, 0.35, 0.35, 1)) self.alnp = render.attachNewNode(self.alight) render.setLight(self.alnp) self.dlight = P.DirectionalLight('dlight') self.dlight.setColor(P.VBase4(0.4, 0.4, 0.4, 1)) self.dlnp = render.attachNewNode(self.dlight) self.dlnp.setHpr(45, -45, 0) render.setLight(self.dlnp) # Setup some scene-wide exponential fog. colour = (0.5,0.8,0.8) self.expfog = P.Fog("Scene-wide exponential Fog object") self.expfog.setColor(*colour) self.expfog.setExpDensity(0.0005) render.setFog(self.expfog) base.setBackgroundColor(*colour) # Setup keyboard controls. # Accept some keys to move the camera. self.showSolids = False self.showCollisions = False self.accept("a", self.camera.setControl, ["left",1]) self.accept("a-up", self.camera.setControl, ["left",0]) self.accept("d", self.camera.setControl, ["right",1]) self.accept("d-up", self.camera.setControl, ["right",0]) self.accept("w", self.camera.setControl, ["up",1]) self.accept("w-up", self.camera.setControl, ["up",0]) self.accept("s", self.camera.setControl, ["down",1]) self.accept("s-up", self.camera.setControl, ["down",0]) self.accept("arrow_up", self.camera.setControl, ["forward",1]) self.accept("arrow_up-up", self.camera.setControl, ["forward",0]) self.accept("arrow_down", self.camera.setControl, ["backward",1]) self.accept("arrow_down-up", self.camera.setControl, ["backward",0]) self.accept("arrow_left", self.camera.setControl, ["strafe-left",1]) self.accept("arrow_left-up", self.camera.setControl, ["strafe-left",0]) self.accept("arrow_right", self.camera.setControl, ["strafe-right",1]) self.accept("arrow_right-up", self.camera.setControl,["strafe-right",0]) # Accept the Esc key to exit. self.accept("escape", sys.exit) # r key restarts the current simulation. self.accept("r",self.restart) # tab key goes to next demonstration self.accept("tab",self.next) # x key toggles wireframe rendering self.accept("x",base.toggleWireframe) # c toggles rendering collisions self.accept("c",self.toggleCollisions) # Lists to hold the Characters and Obstacles currently in the scene. self.characters = [] self.obstacles = [] # Find and initialise all demo plugins. Plugins are loaded in order of # filename. self.plugins = [] self.plugin = 0 for file in sorted(os.listdir('plugins')): if file.endswith('.py'): modulename = file[:-3] try: file, filename, description = imp.find_module( modulename, ['plugins']) pluginmodule = imp.load_module(modulename, file, filename, description) if hasattr(pluginmodule, 'Plugin'): print 'Instantiating plugin from %s' % pluginmodule try: self.plugins.append(pluginmodule.Plugin()) except Exception, e: print 'Urk! %s' % e else: print 'No plugin in %s' % pluginmodule finally: if file: file.close() # Create OnscreenText object. self.text = OnscreenText(text = '', pos = (-1.3, 0.9), scale = 0.05, fg = (0,0,0,1), align=P.TextNode.ALeft) # Setup CollisionRay and CollisionHandlerQueue for mouse picking. self.pickerQ = P.CollisionHandlerQueue() self.picker = camera.attachNewNode(P.CollisionNode('Picker CollisionNode')) self.picker.node().addSolid(P.CollisionRay()) self.picker.node().setFromCollideMask(floorMASK) # Sets into collide masks recursively self.picker.setCollideMask(P.BitMask32.allOff()) # Uncomment this line to make the CollisionRay visible: #self.picker.show() base.cTrav.addCollider(self.picker,self.pickerQ) try: handler.addCollider(self.picker,camera) except: pass self.accept('mouse1',self.onClick) # Start the demo. self.restart() taskMgr.add(self.step,"World task") def toggleCollisions(self): if self.showCollisions: self.showCollisions = False else: self.showCollisions = True if self.showCollisions: base.cTrav.showCollisions(render) else: base.cTrav.hideCollisions() def onClick(self): """Handle the click event.""" mpos=base.mouseWatcherNode.getMouse() # Makes the ray's origin the camera and make the ray point to mpos self.picker.node().getSolid(0).setFromLens( base.camNode,mpos.getX(),mpos.getY()) # We don't want to traverse now, so wait for panda to do it then move. taskMgr.doMethodLater(.02,self.__setDestination,'setDest') def __setDestination(self,task): """Helper method for OnClick. Find the position of the nearest intersection in renderspace.""" if self.pickerQ.getNumEntries() > 0: self.pickerQ.sortEntries() self.point=self.pickerQ.getEntry(0).getSurfacePoint(render) #self.marker.prime.setPos(self.point) # Marker indicates destination self.plugins[self.plugin].click(self.point) def step(self,task): """If the current plugin requests it, wrap Character's around if they leave the terrain.""" # FIXME: hardcoded terrain size and position here. if self.plugins[self.plugin].wrap: for character in self.characters: if character.getspeed() > 0: if character._pos.getX()<-50: character._pos.setX(45) if character._pos.getX()>45: character._pos.setX(-50) if character._pos.getY()<-50: character._pos.setY(45) if character._pos.getY()>45: character._pos.setY(-50) return Task.cont def next(self): """Move onto the next plugin, or go back to the first.""" self.plugin += 1 if self.plugin >= len(self.plugins): self.plugin = 0 self.restart() def restart(self): """Restart the demo environment, its characters, and the current plugin.""" # Destroy the Character objects. for character in self.characters: character.destroy() self.characters = [] # Python will garbage-collect them now # Create new Character objects for the plugin. for i in range(0,self.plugins[self.plugin].numVehicles): self.characters.append(Character('Ralph')) # FIXME: Give each character a different name! self.plugins[self.plugin].vehicles = self.characters self.plugins[self.plugin].restart() # Destroy the Obstacle objects. for obstacle in self.obstacles: obstacle.destroy() self.obstacles = [] # Python will garbage-collect them now # Create new Obstacle objects for the plugin. if self.plugins[self.plugin].obstaclesOn == True: self.obstacles = [SphereObstacle(-20,20,0,15), SphereObstacle(20,10,0,5), SphereObstacle(10,20,0,3), SphereObstacle(20,-15,0,10), SphereObstacle(20,-35,0,3), SphereObstacle(-3,-35,0,10), SphereObstacle(-25,-10,0,7), SphereObstacle(-25,-30,0,2), SphereObstacle(50,-50,0,10), SphereObstacle(-50,-50,0,10), SphereObstacle(-50,50,0,10), SphereObstacle(50,50,0,10) ] self.plugins[self.plugin].obstacles = self.obstacles text = """ r: restart this demo Tab: go to next demo w: look down s: look up a: look left d: look right Arrow keys: move camera forward, backward, left right x: toggle wireframe c: show/hide collisions Esc: exit""" self.text.setText(self.plugins[self.plugin].text + text) # Run the test scene. s = SteerTest() run()