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