All pastes #389139 Raw Edit

Unnamed

public text v1 · immutable
·published 2007-03-10 15:10 UTC
rendered paste body
"""

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()