Thank you all, gentlemen! Thanks to your advices I figured this out.
(I have asked few other questions related to this code, in this thread: discourse.panda3d.org/viewtopic.php?t=3818)
Another question: in the manual it is said that Roaming Ralph uses very inefficient collision method in order to update Ralph’s Z coordinate. What collision method is better? Where can I find better samples and study them?
In order to help newbies like me in future, here I post the working code with commentaries:
# This code was created with Crimsonland-like shooter concept in my head.
# This is why I placed camera so high and removed camera collisions. They
# can be easily restored, having the original Roaming Ralph sample
# and this code.
# Thanks to all who help to make this code!
import direct.directbase.DirectStart
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import WindowProperties
from pandac.PandaModules import PandaNode,NodePath,Camera,Vec3,Filename
from pandac.PandaModules import CollisionTraverser,CollisionNode
from pandac.PandaModules import CollisionHandlerQueue,CollisionRay,BitMask32
import sys
# Path to Roaming Ralph's models directory
modelsDir = "Roaming-Ralph/models/"
# Ralph's speed, units per second
speed = 3
class World(DirectObject):
def __init__(self):
# Disable defaul camera controls
base.disableMouse()
# Initial setup
props = WindowProperties()
props.setCursorHidden(True)
base.win.requestProperties(props)
base.setBackgroundColor(0, 0, 0)
# Find the center of the screen
self.screenCenterX = base.win.getProperties().getXSize()/2
self.screenCenterY = base.win.getProperties().getYSize()/2
# Move mouse to the middle of the screen
base.win.movePointer(0, self.screenCenterX, self.screenCenterY)
# Game state variables
# Previous recorded time in milliseconds (will be used for movements)
self.prevtime = 0
# Is Ralph moving now? (will be used to animate Ralph)
self.isMoving = False
# Initial orientation of Ralph, floater, and camera
# (i.e. self.actorPlace) in degrees
self.angle = 0
# Set floater and camera position (relative to self.actorPlace).
# The camera will look at the floater, and not at Ralph himself.
# Right(-) or left(+), forward (-) or backward (+), down(-) or up(+)
self.floaterPos = (0, -2, 1)
self.cameraPos = (0, 15, 10)
# When we scale nodes, their local coordinate system is scaled too.
# Also, some models use Y as top-down axis, while the engine uses Z
# for it. Therefore, in order to avoid confusion, it's better to
# create an empty node (here it's called "actorPlace"), and reparent
# Ralph to it. In this case Ralph can be scaled as he needs,
# and have Y as his top-down axis, still, we can move, rotate or use
# him in any other way with ease
# Create Actor's placeholder
self.actorPlace = NodePath(PandaNode("actorPlace"))
self.actorPlace.reparentTo(render)
self.actorPlace.setPos(0, 0, 0)
self.actorPlace.setHpr(self.angle, 0, 0)
# Create the main character, Ralph, with two animations
# and reparent him to the self.actorPlace
self.ralph = Actor(modelsDir+"ralph",
{"run":modelsDir+"ralph-run",
"walk":modelsDir+"ralph-walk"})
self.ralph.reparentTo(self.actorPlace)
self.ralph.setScale(.2)
self.ralph.setPos(0, 0, 0)
self.ralph.setHpr(0, 0, 0)
# Create the floater and reparent it to the self.actorPlace
self.floater = NodePath(PandaNode("floater"))
self.floater.reparentTo(self.actorPlace)
self.floater.setPos(self.floaterPos[0], self.floaterPos[1], self.floaterPos[2])
# Create the camera, reparent it to the self.actorPlace
# and make it look at the floater
base.camera.reparentTo(self.actorPlace)
base.camera.setPos(self.cameraPos[0], self.cameraPos[1], self.cameraPos[2])
base.camera.lookAt(self.floater)
# Load world
self.environ = loader.loadModel(modelsDir+"world")
self.environ.reparentTo(render)
self.environ.setPos(0, 0, 0)
# Create key map
# (primFire and secFire are not used, but reserved for future use)
self.keyMap = {"left":0,
"right":0,
"forward":0,
"backward":0,
"changeView":0,
"primFire":0,
"secFire":0}
# Accept the control keys for movement and rotation
# (primFire and secFire are not used, but reserved for future use)
self.accept("escape", sys.exit)
self.accept("w", self.setKey, ["forward",1])
self.accept("a", self.setKey, ["left",1])
self.accept("s", self.setKey, ["backward",1])
self.accept("d", self.setKey, ["right",1])
self.accept("w-up", self.setKey, ["forward",0])
self.accept("a-up", self.setKey, ["left",0])
self.accept("s-up", self.setKey, ["backward",0])
self.accept("d-up", self.setKey, ["right",0])
self.accept("mouse2", self.setKey, ["changeView",1])
self.accept("mouse2-up", self.setKey, ["changeView",0])
self.accept("mouse1", self.setKey, ["primFire",1])
self.accept("mouse1-up", self.setKey, ["primFire",0])
self.accept("mouse3", self.setKey, ["secFire",1])
self.accept("mouse3-up", self.setKey, ["secFire",0])
# We repeat self.move every frame
taskMgr.add(self.move,"moveTask")
# Collision detection
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)
# Records the state of the arrow keys
def setKey(self, key, value):
self.keyMap[key] = value
def move(self, task):
# This method is called every frame, since it is attached to taskMgr.
# The elapsed time is the current time minus the last saved time
elapsed = task.time - self.prevtime
# Save Ralph's initial position so that we can restore it,
# in case he falls off the map or runs into something.
startpos = self.actorPlace.getPos()
# If a move key is pressed, turn ralph in the specified direction.
if (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0) or (self.keyMap["backward"]!=0) or (self.keyMap["forward"]!=0):
rotDirections = (self.keyMap["left"], self.keyMap["right"], self.keyMap["backward"], self.keyMap["forward"])
rotValues = (90, 270, 180, (0, 360))
rotateBy = 0
divideBy = 0
for direction in rotDirections:
divideBy += direction
for value in range(0, 4):
if value != 3:
rotateBy += rotDirections[value]*rotValues[value]
if value == 3:
if self.keyMap["left"]!=0:
rotateBy += rotDirections[value]*rotValues[value][0]
else:
rotateBy += rotDirections[value]*rotValues[value][1]
rotateBy = rotateBy / divideBy
self.ralph.setH(rotateBy)
# If a move-key is pressed, move ralph in the specified direction.
if (self.keyMap["left"]!=0):
self.actorPlace.setPos(self.actorPlace, Vec3(1, 0, 0)*(elapsed*speed))
if (self.keyMap["right"]!=0):
self.actorPlace.setPos(self.actorPlace, Vec3(-1, 0, 0)*(elapsed*speed))
if (self.keyMap["forward"]!=0):
self.actorPlace.setPos(self.actorPlace, Vec3(0, -1, 0)*(elapsed*speed))
if (self.keyMap["backward"]!=0):
self.actorPlace.setPos(self.actorPlace, Vec3(0, 1, 0)*(elapsed*speed))
# If ralph is moving, loop "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) or (self.keyMap["backward"]!=0):
if not self.isMoving:
self.ralph.loop("run")
self.isMoving = True
else:
if self.isMoving:
self.ralph.stop()
self.ralph.pose("walk",5)
self.isMoving = False
# Mouse controls
if self.keyMap["changeView"]!=0: # If changeView button is pressed, change view when moving mouse.
mouseX = base.win.getPointer(0).getX()
# Update ratation
if base.win.movePointer(0, self.screenCenterX, self.screenCenterY):
self.angle -= (mouseX - self.screenCenterX)
if self.angle < 0:
self.angle += 360
if self.angle >= 360:
self.angle -= 360
# Otherwise, just keep cursor position at the center of the screen
else:
base.win.movePointer(0, self.screenCenterX, self.screenCenterY)
# Update actorPlace orientation
self.actorPlace.setH(self.angle)
# 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.actorPlace.setZ(entries[0].getSurfacePoint(render).getZ())
else:
self.actorPlace.setPos(startpos)
# Store the task time and continue
self.prevtime = task.time
return Task.cont
# Create world
World()
run()