I Need Some Help With My Player Collision Script

Hi, I’ve been struggling with this code for days. I’m trying to make the player model walk over uneven terrain using ‘mouse point and click’ controls instead of keyboard controls.

The point and click controls work perfectly, but my collision detection leaves a lot to be desired. As you can see from the picture, the player is moving above the ground (he does start with his feet on the ground) but as soon as I click a spot on the terrain ‘up he goes’ he doesn’t follow the contours of the terrain at all, most of the time he’s either above the ground or plowing right through the middle of the hills :confused:

I’ve tried all sorts of different things to make this work, but I’ve now run out of ideas. Can anybody see where I’ve gone wrong? I’ve marked the relevant section in red:

I really hope somebody can help.

Thanks a lot.

Ralph’s origin originally exactly on his feet.
Why did you set Ralph’s Z position to 1.7 ?
That’s why he keep floating above the ground.

Hi Jo. Thanks for trying to help. My terrain model doesn’t have a start position, so I set Ralph’s Z position to 1.7 so that he would start with his feet on the ground (instead of being sunk into it).

Anyway, I tried setting his start position back to (0,0,0) as you suggested and it has improved things (he’s now on the ground :smiley:), but he’s still floating from hill to hill, instead of following the up & down contours of the ground. I made a picture to illustrate the problem:

What have I done wrong?

Cheers

i don’t see where the avatars Z is corrected from the collision ray with the ground. you are missing the setZ to correct the value.

i’m sure this is done in the ralph demo - just look for it and compare to your code.

maybe you can re-post the code with correct indention?

hth,
kw.

Ok here is the updated code (it’s still not working :confused:):

# Left click on the ground to move.
# Rotate the camera by moving the mouse pointer to the edges of the screen or
# with the left & right arrow keys.
# Zoom the camera with the mouse wheel or the up & down arrow keys.

import direct.directbase.DirectStart # Start Panda
from pandac.PandaModules import * # Import the Panda Modules
from direct.showbase.DirectObject import DirectObject # To handle Events
from direct.task import Task # To use Tasks
from direct.actor import Actor # To use animated Actors
from direct.interval.IntervalGlobal import * # To use Intervals
from direct.showbase.PythonUtil import closestDestAngle # For player rotation
import sys

class Controls(DirectObject):
    def __init__(self):
        base.disableMouse()
        self.loadModels()
        self.setupCollisions()
        # Declare variables.
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 6.0 # Controls how long it takes the player to 
        # move to the clicked destination. 
        self.speed = .10 # Controls the speed of the camera rotation and zoom.
        # Setup controls
        self.accept("escape", sys.exit)
        self.accept("player-stopped", self.stopWalkAnim)
        self.accept('mouse1', self.moveToPosition)
        self.accept("arrow_left", self.cameraTurn,[-1])
        self.accept("arrow_right", self.cameraTurn,[1])
        self.accept("arrow_up", self.cameraZoom,[-1])
        self.accept("arrow_down", self.cameraZoom,[1])
        self.accept("wheel_up", self.cameraZoom,[-1])
        self.accept("wheel_down", self.cameraZoom,[1])
        taskMgr.add(self.edgeScreenTracking, "edgeScreenTracking")
        # End __init__
        
    def loadModels(self):
        # Load an environment
        self.environ = loader.loadModel("models/bumpy_grass")
        self.environ.reparentTo(render) # Place it in the scene.
        self.environ.setPos(0, 0, 0)
        self.environ.setHpr(0, 0, 0)
        # Create a player dummy node
        self.player_dummy_node = render.attachNewNode("player_dummy_node")
        # Position it at the center of the world
        self.player_dummy_node.setPos(0, 0, 0)
        self.player_dummy_node.setHpr(0, 0, 0)
        # Load the player and its animations
        self.player = Actor.Actor("models/ralph",{"walk":"models/ralph-walk"})
        # Attach the player to the player dummy node
        self.player.reparentTo(self.player_dummy_node)
        self.player.setScale(.5)
        self.player.setPos(0, 0, 0)
        # Create a dummy node for the player turn function
        self.npLook = render.attachNewNode("npLook")
        #Create a camera dummy node
        self.camera_dummy_node = render.attachNewNode("camera_dummy_node")
        # Attach the camera dummy node to the player dummy node.
        self.camera_dummy_node.reparentTo(self.player_dummy_node)
        # Attach the camera to the camera dummy node.
        camera.reparentTo(self.camera_dummy_node)
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # End loadModels
        
    # Define a function to setup collision detection. We need two rays, one
    # attached to the camera for mouse picking and one attached to the player
    # for collision with the terrain. The rays must only cause collisions and 
    # not collide with each other so their Into bitMasks are set to allOff().
    def setupCollisions(self):
        # The terrain model was edited by hand to include the following tag:
        # <Collide> Plane01 { Polyset keep descend }.
        #Once we have the collision tags in the model, we can get to them using
        #NodePath's find command.
        self.ground = self.environ.find("**/Plane01")
        # Set the model's Into collide mask to bit (0).
        self.ground.node().setIntoCollideMask(BitMask32.bit(0))
        # Create a CollisionTraverser. CollisionTraversers are what do the 
        # job of calculating collisions.
        self.picker = CollisionTraverser() 
        # Create a handler for the picker ray
        self.queue = CollisionHandlerQueue() 
        #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)
        # Set the collision node's From collide mask. Now the ray can only cause 
        # collisions with objects that have bitMask(0)
        self.pickerNode.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask. The ray cannot be collided
        # into.
        self.pickerNode.setIntoCollideMask(BitMask32.allOff())
        # Make our ray
        self.pickerRay = CollisionRay()
        # Add it to the collision node  
        self.pickerNode.addSolid(self.pickerRay) 
        #Register the ray as something that can cause collisions with the traverser
        self.picker.addCollider(self.pickerNP, self.queue)
        
        # Setup collision stuff to handle the player's collision with the terrain.
        # Make a collision node for the player's ray
        self.groundCol = CollisionNode('playerRay')
        # Make a collision ray for the player
        self.groundRay = CollisionRay()
        # Set the height of the ray (7 units above the player's head)
        self.groundRay.setOrigin(0,0,7)
        # Set the rays direction (pointing down on the Z axis)
        self.groundRay.setDirection(0,0,-1)
        # Add the collision node to the collision ray
        self.groundCol.addSolid(self.groundRay)
        # Set the collision node's From collide mask. The ray can only collide
        # with objects that have bitMask(0).
        self.groundCol.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask to allOff so that nothing 
        # can collide into the ray.
        self.groundCol.setIntoCollideMask(BitMask32.allOff())
        # Attach the collision node to the player dummy node
        self.groundColNp = self.player_dummy_node.attachNewNode(self.groundCol)
        self.groundColNp.setZ(-1)
        # Make a handler for the ground ray
        self.floorHandler = CollisionHandlerFloor()
        # Associate it with the player dummy node
        self.floorHandler.addCollider(self.groundColNp, self.player_dummy_node)
        # Register the ray as something that can cause collisions with the traverser
        self.picker.addCollider(self.groundColNp, self.floorHandler)
        
        # Uncomment this line to see the collision rays
        self.groundColNp.show()
        #Uncomment this line to show a visual representation of the 
        #collisions occuring
        self.picker.showCollisions(render)
        # End setupCollisions
        
    # Define a task to monitor the position of the mouse pointer & rotate 
    # the camera when the mouse pointer moves to the edges of the screen.
    def edgeScreenTracking(self,task):
        # Check if the mouse is available
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            self.cameraTurn(1)
        elif mpos.getX() < -0.99:
            self.cameraTurn(-1)
        return Task.cont 
        # End edgeScreenTracking
        
    # Define the CameraTurn function.
    def cameraTurn(self,dir):
        self.camTurn = LerpHprInterval(self.camera_dummy_node, self.speed, Point3(self.camera_dummy_node.getH()-(10*dir), 0, 0))
        self.camTurn.start()
        # End cameraTurn
    
    # Define the cameraZoom function.
    def cameraZoom(self,dir):
        self.camZoom = LerpPosInterval(camera, self.speed, Point3(camera.getX(), camera.getY()-(2*dir), camera.getZ()))
        self.camZoom.start()
        # End cameraZoom
    
    # Define a function to get the position of the mouse click.
    def getPosition(self, mousepos):
        self.pickerRay.setFromLens(base.camNode, mousepos.getX(),mousepos.getY())
        # Now check for collisions.
        self.picker.traverse(render)
        if self.queue.getNumEntries() > 0:
            self.queue.sortEntries()
            # This is the clicked position.
            self.position = self.queue.getEntry(0).getSurfacePoint(self.environ)
            return None
        # End getPosition
    
    # Define a function to make the player turn towards the clicked position
    # and then move to that position.   
    def moveToPosition(self):
        # Get the clicked position
        self.getPosition(base.mouseWatcherNode.getMouse())
        # Calculate the new hpr
        self.npLook.setPos(self.player.getPos(render))
        # Look at the clicked position.
        self.npLook.lookAt(self.position)
        # Prevent turn 'wrapping' 
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH)
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr()
        newH = closestDestAngle(currHpr[0], newHpr[0])
        # Create a turn animation from current hpr to the calculated new hpr.
        playerTurn = self.player.hprInterval(.1, Point3(newH, newHpr[1], newHpr[2]))
        # Calculate the distance between the start and finish positions.
        # This is then used to calculate the duration it should take to
        # travel to the new coordinates based on self.movementSpeed
        travelVec = self.position - self.player_dummy_node.getPos()
        distance = travelVec.length()
        # Create an animation to make the player move to the clicked position
        playerMove = self.player_dummy_node.posInterval((distance / self.movementSpeed), self.position)
        # Put both animations into a sequence
        self.playerMovement = Sequence(playerTurn, playerMove)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()
        # End moveToPosition

    def stopWalkAnim(self):
        # This is called when the movement animation has finished.
        # We can then stop the walk animation.
        self.player.stop("walk")
        self.player.pose("walk",17)
        self.playerMove = None 
        
c = Controls()

run()

I added this line to it:

  self.groundColNp.setZ(-1)

But obviously that’s not right sigh. If possible could somebody tell me which line or lines I need to change?

Thanks a lot.

essentially what is happening is, that the code is not correcting the avatars Z position at all.

you use two intervals to turn and walk, but they do not take the height into account. while the avatar walks he would need to adjust to the height. there is no code that does that.

ralph works like that: every frame it will check the collision ray from ralphs head to the ground, find the collision point (which is at the ground) and adjust ralphs Z position according to this position.

essentially you would need to code that piece and integrate this with your intervals (maybe a parallel interval that does the correction).

hth,
kaweh

Hmmm, I’ll see if I can get it to work. This is the hardest challenge I’ve had so far :confused:.

Thanks a lot.

It looks like you are moving using intervals. Interval don’t work with the collision pushers very well. The best you can do is try adding the parameter fluid=1 to the posInterval 's. See the improved Pusher Example

Hi Cyan. What does fluid = 1 do exactly? I haven’t seen it used before.

Does anybody know of any examples that might help me to get this working? I’m not very good at programming you see, so I learn by studying and editing other people’s code.

Has anybody else managed to get mouse point & click controls and collision detection working together? Cause I’d really like to look at your code :unamused:.

Thanks a lot.

something along these lines (though i haven’t really tested that; be aware that’s only a code snippet which needs to be inserted in your code correctly):

    def moveToPosition(self): 
        ...

        # Create an animation to make the player move to the clicked position 
        playerMove = self.player_dummy_node.posInterval((distance / self.movementSpeed), self.position) 
        # We create a LerpFunc Interval to correct the Z as we go along
        playerPositionZ = LerpFunc(self.correctPlayerZ, duration=(distance / self.movementSpeed))
        # Put both animations into a sequence 
        self.playerMovement = Parallel(Sequence(playerTurn, playerMove), playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped") 
        self.player.loop("walk") 
        self.playerMovement.start() 
        # End moveToPosition 
        
    def correctPlayerZ(self, time):
        startpos = self.player_dummy_node.getPos()
        self.picker.traverse(render) 
        if self.floorHandler.getNumEntries() > 0:
            point = self.queue.getEntry(0).getSurfacePoint(self.environ)
            self.player.setZ(point.getZ())
        else:
            self.player.setPos(startpos)

what you do:

  • create a LerpFunc interval and put it in parallel to your movement/turning Interval Sequence; the function given to the LerpFunc interval will be called in parallel to your other intervals

  • implement a ‘correctPlayerZ’ function (the function that we provide to the LerpFunc interval) that will read the collision (thus the Z position) and correct the player models Z position accordingly.

i have personally implemented the z correction without intervals so i’m not sure if that will 100% solve your problem.

hth,
kaweh

Kaweh I can’t thankyou enough for this, this is exactly what I needed. Thankyou very, very much.

I’ve just added your code to my script as follows:

# Left click on the ground to move.
# Rotate the camera by moving the mouse pointer to the edges of the screen or
# with the left & right arrow keys.
# Zoom the camera with the mouse wheel or the up & down arrow keys.

import direct.directbase.DirectStart # Start Panda
from pandac.PandaModules import * # Import the Panda Modules
from direct.showbase.DirectObject import DirectObject # To handle Events
from direct.task import Task # To use Tasks
from direct.actor import Actor # To use animated Actors
from direct.interval.IntervalGlobal import * # To use Intervals
from direct.showbase.PythonUtil import closestDestAngle # For player rotation
import sys

class Controls(DirectObject):
    def __init__(self):
        base.disableMouse()
        self.loadModels()
        self.setupCollisions()
        # Declare variables.
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 6.0 # Controls how long it takes the player to 
        # move to the clicked destination. 
        self.speed = .10 # Controls the speed of the camera rotation and zoom.
        # Setup controls
        self.accept("escape", sys.exit)
        self.accept("player-stopped", self.stopWalkAnim)
        self.accept('mouse1', self.moveToPosition)
        self.accept("arrow_left", self.cameraTurn,[-1])
        self.accept("arrow_right", self.cameraTurn,[1])
        self.accept("arrow_up", self.cameraZoom,[-1])
        self.accept("arrow_down", self.cameraZoom,[1])
        self.accept("wheel_up", self.cameraZoom,[-1])
        self.accept("wheel_down", self.cameraZoom,[1])
        taskMgr.add(self.edgeScreenTracking, "edgeScreenTracking")
        # End __init__
        
    def loadModels(self):
        # Load an environment
        self.environ = loader.loadModel("models/bumpy_grass")
        self.environ.reparentTo(render) # Place it in the scene.
        self.environ.setPos(0, 0, 0)
        self.environ.setHpr(0, 0, 0)
        # Create a player dummy node
        self.player_dummy_node = render.attachNewNode("player_dummy_node")
        # Position it at the center of the world
        self.player_dummy_node.setPos(0, 0, 0)
        self.player_dummy_node.setHpr(0, 0, 0)
        # Load the player and its animations
        self.player = Actor.Actor("models/ralph",{"walk":"models/ralph-walk"})
        # Attach the player to the player dummy node
        self.player.reparentTo(self.player_dummy_node)
        self.player.setScale(.5)
        self.player.setPos(0, 0, 0)
        # Create a dummy node for the player turn function
        self.npLook = render.attachNewNode("npLook")
        #Create a camera dummy node
        self.camera_dummy_node = render.attachNewNode("camera_dummy_node")
        # Attach the camera dummy node to the player dummy node.
        self.camera_dummy_node.reparentTo(self.player_dummy_node)
        # Attach the camera to the camera dummy node.
        camera.reparentTo(self.camera_dummy_node)
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # End loadModels
        
    # Define a function to setup collision detection. We need two rays, one
    # attached to the camera for mouse picking and one attached to the player
    # for collision with the terrain. The rays must only cause collisions and 
    # not collide with each other so their Into bitMasks are set to allOff().
    def setupCollisions(self):
        # The terrain model was edited by hand to include the following tag:
        # <Collide> Plane01 { Polyset keep descend }.
        #Once we have the collision tags in the model, we can get to them using
        # the NodePath's find command.
        self.ground = self.environ.find("**/Plane01")
        # Set the model's Into collide mask to bit (0).
        self.ground.node().setIntoCollideMask(BitMask32.bit(0))
        # Create a CollisionTraverser. CollisionTraversers are what do the 
        # job of calculating collisions.
        self.picker = CollisionTraverser() 
        # Create a handler for the picker ray
        self.queue = CollisionHandlerQueue() 
        #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)
        # Set the collision node's From collide mask. Now the ray can only cause 
        # collisions with objects that have bitMask(0)
        self.pickerNode.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask. The ray cannot be collided
        # into.
        self.pickerNode.setIntoCollideMask(BitMask32.allOff())
        # Make our ray
        self.pickerRay = CollisionRay()
        # Add it to the collision node  
        self.pickerNode.addSolid(self.pickerRay) 
        #Register the ray as something that can cause collisions with the traverser
        self.picker.addCollider(self.pickerNP, self.queue)
        
        # Setup collision stuff to handle the player's collision with the terrain.
        # Make a collision node for the player's ray
        self.groundCol = CollisionNode('playerRay')
        # Make a collision ray for the player
        self.groundRay = CollisionRay()
        # Set the height of the ray (7 units above the player's head)
        self.groundRay.setOrigin(0,0,7)
        # Set the rays direction (pointing down on the Z axis)
        self.groundRay.setDirection(0,0,-1)
        # Add the collision node to the collision ray
        self.groundCol.addSolid(self.groundRay)
        # Set the collision node's From collide mask. The ray can only collide
        # with objects that have bitMask(0).
        self.groundCol.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask to allOff so that nothing 
        # can collide into the ray.
        self.groundCol.setIntoCollideMask(BitMask32.allOff())
        # Attach the collision node to the player dummy node
        self.groundColNp = self.player_dummy_node.attachNewNode(self.groundCol)
        # Make a handler for the ground ray
        self.floorHandler = CollisionHandlerFloor()
        # Associate it with the player dummy node
        self.floorHandler.addCollider(self.groundColNp, self.player_dummy_node)
        # Register the ray as something that can cause collisions with the traverser
        self.picker.addCollider(self.groundColNp, self.floorHandler)
        
        # Uncomment this line to see the collision rays
        self.groundColNp.show()
        #Uncomment this line to show a visual representation of the 
        #collisions occuring
        self.picker.showCollisions(render)
        # End setupCollisions
        
    # Define a task to monitor the position of the mouse pointer & rotate 
    # the camera when the mouse pointer moves to the edges of the screen.
    def edgeScreenTracking(self,task):
        # Check if the mouse is available
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            self.cameraTurn(1)
        elif mpos.getX() < -0.99:
            self.cameraTurn(-1)
        return Task.cont 
        # End edgeScreenTracking
        
    # Define the CameraTurn function.
    def cameraTurn(self,dir):
        self.camTurn = LerpHprInterval(self.camera_dummy_node, self.speed, Point3(self.camera_dummy_node.getH()-(10*dir), 0, 0))
        self.camTurn.start()
        # End cameraTurn
    
    # Define the cameraZoom function.
    def cameraZoom(self,dir):
        self.camZoom = LerpPosInterval(camera, self.speed, Point3(camera.getX(), camera.getY()-(2*dir), camera.getZ()))
        self.camZoom.start()
        # End cameraZoom
    
    # Define a function to get the position of the mouse click.
    def getPosition(self, mousepos):
        startpos = self.player.getPos()
        self.pickerRay.setFromLens(base.camNode, mousepos.getX(),mousepos.getY())
        # Now check for collisions.
        self.picker.traverse(render)
        if self.queue.getNumEntries() > 0:
            self.queue.sortEntries()
            # This is the clicked position.
            self.position = self.queue.getEntry(0).getSurfacePoint(self.environ)
            return None
        # End getPosition
    
    # Define a function to make the player turn towards the clicked position
    # and then move to that position.   
    def moveToPosition(self):
        # Get the clicked position
        self.getPosition(base.mouseWatcherNode.getMouse())
        # Calculate the new hpr
        self.npLook.setPos(self.player.getPos(render))
        # Look at the clicked position.
        self.npLook.lookAt(self.position)
        # Prevent overturning or 'wrapping' 
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH)
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr()
        newH = closestDestAngle(currHpr[0], newHpr[0])
        # Create a turn animation from current hpr to the calculated new hpr.
        playerTurn = self.player.hprInterval(.1, Point3(newH, newHpr[1], newHpr[2]))
        # Calculate the distance between the start and finish positions.
        # This is then used to calculate the duration it should take to
        # travel to the new coordinates based on self.movementSpeed
        travelVec = self.position - self.player_dummy_node.getPos()
        distance = travelVec.length()
        # Create an animation to make the player move to the clicked position
        playerMove = self.player_dummy_node.posInterval((distance / self.movementSpeed), self.position)
        # We create a LerpFunc Interval to correct the Z as we go along
        playerPositionZ = LerpFunc(self.correctPlayerZ, duration=(distance / self.movementSpeed))
        # Put both animations into a sequence
        self.playerMovement = Parallel(Sequence(playerTurn, playerMove), playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()
        # End moveToPosition
       
    def correctPlayerZ(self, time):
        startpos = self.player_dummy_node.getPos()
        self.picker.traverse(render)
        if self.floorHandler.getNumEntries() > 0:
            point = self.queue.getEntry(0).getSurfacePoint(self.environ)
            self.player.setZ(point.getZ())
        else:
            self.player.setPos(startpos)
        # End correctPlayerZ

    def stopWalkAnim(self):
        # This is called when the movement animation has finished.
        # We can then stop the walk animation.
        self.player.stop("walk")
        self.player.pose("walk",17)
        self.playerMove = None 
        
c = Controls()

run()

But when I ran it I got this error message:

if self.floorHandler.getNumEntries() > 0:
AttributeError: 'libpanda.CollisionHandlerFloor' object has no attribute 'getNumEntries'

I’ve stuffed something up again sigh. Can you see what I did wrong?

Thanks heaps

AttributeError: 'libpanda.CollisionHandlerFloor' object has no attribute 'getNumEntries'

getNumEntries is a method of CollisionHandlerQueue.

So, you’re no longer using CollisionHandlerFloor, but correcting the Z position manually instead.
After playing around, I did these changes:

  1. the movement is calculated only on coplanar surface, i.e. z=0 in the getPosition(). So the player_dummy_node will keep on the Z=0, and the one who moves up and down is just Ralph.
  2. attach the collision ray to Ralph, not to the dummy node
  3. I added a new collision traverser : Zcoll and a new collision handler queue. It’s to accomodate the Z-correction only, which is traversed everyframe. You don’t want to traverse self.picker every frame, because it will keep the mouse ray collision updated too, moving accordingly as the player move.

Try it yourself :

# Left click on the ground to move.
# Rotate the camera by moving the mouse pointer to the edges of the screen or
# with the left & right arrow keys.
# Zoom the camera with the mouse wheel or the up & down arrow keys.

import direct.directbase.DirectStart # Start Panda
from pandac.PandaModules import * # Import the Panda Modules
from direct.showbase.DirectObject import DirectObject # To handle Events
from direct.task import Task # To use Tasks
from direct.actor import Actor # To use animated Actors
from direct.interval.IntervalGlobal import * # To use Intervals
from direct.showbase.PythonUtil import closestDestAngle # For player rotation
import sys

class Controls(DirectObject):
    def __init__(self):
        base.disableMouse()
        self.loadModels()
        self.setupCollisions()
        # Declare variables.
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 6.0 # Controls how long it takes the player to
        # move to the clicked destination.
        self.speed = .10 # Controls the speed of the camera rotation and zoom.
        # Setup controls
        self.accept("escape", sys.exit)
        self.accept("player-stopped", self.stopWalkAnim)
        self.accept('mouse1', self.moveToPosition)
        self.accept("arrow_left", self.cameraTurn,[-1])
        self.accept("arrow_right", self.cameraTurn,[1])
        self.accept("arrow_up", self.cameraZoom,[-1])
        self.accept("arrow_down", self.cameraZoom,[1])
        self.accept("wheel_up", self.cameraZoom,[-1])
        self.accept("wheel_down", self.cameraZoom,[1])
        taskMgr.add(self.edgeScreenTracking, "edgeScreenTracking")
        # End __init__
       
    def loadModels(self):
        # Load an environment
        self.environ = loader.loadModel("models/bumpy_grass")
        self.environ.reparentTo(render) # Place it in the scene.
        self.environ.setPos(0, 0, 0)
        self.environ.setHpr(0, 0, 0)
        # Create a player dummy node
        self.player_dummy_node = render.attachNewNode("player_dummy_node")
        # Position it at the center of the world
        self.player_dummy_node.setPos(0, 0, 0)
        self.player_dummy_node.setHpr(0, 0, 0)
        # Load the player and its animations
        self.player = Actor.Actor("models/ralph",{"walk":"models/ralph-walk"})
        # Attach the player to the player dummy node
        self.player.reparentTo(self.player_dummy_node)
        self.player.setScale(.5)
        self.player.setPos(0, 0, 0)
        # Create a dummy node for the player turn function
        self.npLook = render.attachNewNode("npLook")
        #Create a camera dummy node
        self.camera_dummy_node = render.attachNewNode("camera_dummy_node")
        # Attach the camera dummy node to the player dummy node.
        self.camera_dummy_node.reparentTo(self.player_dummy_node)
        # Attach the camera to the camera dummy node.
        camera.reparentTo(self.camera_dummy_node)
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # End loadModels
       
    # Define a function to setup collision detection. We need two rays, one
    # attached to the camera for mouse picking and one attached to the player
    # for collision with the terrain. The rays must only cause collisions and
    # not collide with each other so their Into bitMasks are set to allOff().
    def setupCollisions(self):
        # The terrain model was edited by hand to include the following tag:
        # <Collide> Plane01 { Polyset keep descend }.
        #Once we have the collision tags in the model, we can get to them using
        # the NodePath's find command.
        self.ground = self.environ.find("**/Plane01")
        # Set the model's Into collide mask to bit (0).
        self.ground.node().setIntoCollideMask(BitMask32.bit(0))
        # Create a CollisionTraverser. CollisionTraversers are what do the
        # job of calculating collisions.
        self.picker = CollisionTraverser()
        # Create a handler for the picker ray
        self.queue = CollisionHandlerQueue()
        #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)
        # Set the collision node's From collide mask. Now the ray can only cause
        # collisions with objects that have bitMask(0)
        self.pickerNode.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask. The ray cannot be collided
        # into.
        self.pickerNode.setIntoCollideMask(BitMask32.allOff())
        # Make our ray
        self.pickerRay = CollisionRay()
        # Add it to the collision node 
        self.pickerNode.addSolid(self.pickerRay)
        #Register the ray as something that can cause collisions with the traverser
        self.picker.addCollider(self.pickerNP, self.queue)
       
        # Setup collision stuff to handle the player's collision with the terrain.
        # Make a collision node for the player's ray
        self.groundCol = CollisionNode('playerRay')
        # Make a collision ray for the player
        self.groundRay = CollisionRay()
        # Attach the collision node to the player dummy node
        self.groundColNp = self.player.attachNewNode(self.groundCol)
        # Set the height of the ray (7 units above the player's head)
        self.groundRay.setOrigin(0,0,7)
        # Set the rays direction (pointing down on the Z axis)
        self.groundRay.setDirection(0,0,-1)
        # Add the collision node to the collision ray
        self.groundCol.addSolid(self.groundRay)
        # Set the collision node's From collide mask. The ray can only collide
        # with objects that have bitMask(0).
        self.groundCol.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask to allOff so that nothing
        # can collide into the ray.
        self.groundCol.setIntoCollideMask(BitMask32.allOff())
        # Make a handler for the ground ray
        self.floorHandler = CollisionHandlerFloor()
        # Associate it with the player dummy node
        self.floorHandler.addCollider(self.groundColNp, self.player_dummy_node)
        # Register the ray as something that can cause collisions with the traverser
#        self.picker.addCollider(self.groundColNp, self.floorHandler)
       
        self.Zcoll = CollisionTraverser()
        self.ZcollQueue = CollisionHandlerQueue()
        self.Zcoll.addCollider(self.groundColNp, self.ZcollQueue)
        self.Zcoll.showCollisions(render)

        # Uncomment this line to see the collision rays
        self.groundColNp.show()
        #Uncomment this line to show a visual representation of the
        #collisions occuring
#        self.picker.showCollisions(render)
        # End setupCollisions

       
    # Define a task to monitor the position of the mouse pointer & rotate
    # the camera when the mouse pointer moves to the edges of the screen.
    def edgeScreenTracking(self,task):
        # Check if the mouse is available
        if not base.mouseWatcherNode.hasMouse():
            return Task.cont
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            self.cameraTurn(1)
        elif mpos.getX() < -0.99:
            self.cameraTurn(-1)
        return Task.cont
        # End edgeScreenTracking
       
    # Define the CameraTurn function.
    def cameraTurn(self,dir):
        self.camTurn = LerpHprInterval(self.camera_dummy_node, self.speed, Point3(self.camera_dummy_node.getH()-(10*dir), 0, 0))
        self.camTurn.start()
        # End cameraTurn
   
    # Define the cameraZoom function.
    def cameraZoom(self,dir):
        self.camZoom = LerpPosInterval(camera, self.speed, Point3(camera.getX(), camera.getY()-(2*dir), camera.getZ()))
        self.camZoom.start()
        # End cameraZoom
   
    # Define a function to get the position of the mouse click.
    def getPosition(self, mousepos):
        startpos = self.player.getPos()
        self.pickerRay.setFromLens(base.camNode, mousepos.getX(),mousepos.getY())
        # Now check for collisions.
        self.picker.traverse(render)
        if self.queue.getNumEntries() > 0:
            self.queue.sortEntries()
            # This is the clicked position.
            self.position = self.queue.getEntry(0).getSurfacePoint(self.environ)
####################################
####################################
            self.position.setZ(0)
####################################
####################################
            return None
        # End getPosition
   
    # Define a function to make the player turn towards the clicked position
    # and then move to that position.   
    def moveToPosition(self):
        # Get the clicked position
        self.getPosition(base.mouseWatcherNode.getMouse())
        # Calculate the new hpr
        self.npLook.setPos(self.player.getPos(render))
        # Look at the clicked position.
        self.npLook.lookAt(self.position)
        # Prevent overturning or 'wrapping'
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH)
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr()
        newH = closestDestAngle(currHpr[0], newHpr[0])
        # Create a turn animation from current hpr to the calculated new hpr.
        playerTurn = self.player.hprInterval(.1, Point3(newH, newHpr[1], newHpr[2]))
        # Calculate the distance between the start and finish positions.
        # This is then used to calculate the duration it should take to
        # travel to the new coordinates based on self.movementSpeed
        travelVec = self.position - self.player_dummy_node.getPos()
        distance = travelVec.length()
        # Create an animation to make the player move to the clicked position
        playerMove = self.player_dummy_node.posInterval((distance / self.movementSpeed), self.position)
        # We create a LerpFunc Interval to correct the Z as we go along
        playerPositionZ = LerpFunc(self.correctPlayerZ, duration=(distance / self.movementSpeed))
        # Put both animations into a sequence
        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()
        # End moveToPosition
       
    def correctPlayerZ(self, time):
        startpos = self.player.getPos()
        self.Zcoll.traverse(render)

        if self.ZcollQueue.getNumEntries>0:
	   self.ZcollQueue.sortEntries()
	   point = self.ZcollQueue.getEntry(0).getSurfacePoint(self.environ)
	   self.player.setZ(point.getZ())
        else:
           self.player.setPos(startpos)
        # End correctPlayerZ

    def stopWalkAnim(self):
        # This is called when the movement animation has finished.
        # We can then stop the walk animation.
        self.player.stop("walk")
        self.player.pose("walk",17)
        self.playerMove = None
       
c = Controls()

run()

You did it! This is fantastic! Thankyou very, very much. I can’t believe how quickly you solved this. I’ve been struggling with this until my eyes crossed :unamused:.

At least I was on the right track (which is some small comfort) but it would have taken me forever to figure this out on my own. So again, I can’t thankyou enough for helping me with this.

Anyway, what I’m trying to do is make a little Diablo-like demo that could also serve as a script template (or starting point) that people could work from to modify and change as they please (eventually, I’d really like to have templates for each game genre FPS, RTS…and so on).

But for this one, I want it to be pretty much mouse controlled, ie… click a spot on the ground to move there (DONE! :smiley:), click an object to pick it up/put in inventory, click an enemy to attack it, click an NPC to talk to them…etc.

With that in mind, am I going about this the right way? I now get the impression that Intervals aren’t the best thing to use for player movement (I’m just following the tutorials), is there a better way?

Again, thanks to everybody for all your help with this.

Cheers

Man! This is so brilliant, I keep loading it up just to play around with it :laughing:

I just have one very slight problem though, when going down hill, Ralph leans over (oh! I should mention, the Ralph model was modeled facing backwards, so with point & click movement controls he moves backwards as well :confused:):

Yet when he’s going up hill, he’s perfectly fine and remains upright:

I checked through the code and can’t see anything to account for this. Any idea’s?

Cheers

I’m still new to Panda3d, but i’d say the easy way to solve this is to reparent Ralph to another dummy node and rotate the original 180 degrees.

To flip Ralph’s heading, just add 180 deg to his new heading.

If you want Ralph stays upright, why do you mess up with his pitch and roll :

playerTurn = self.player.hprInterval(.1, Point3(newH, newHpr[1], newHpr[2]))

To fix them both, add 180 deg to the heading, and set the new pitch & roll to 0 :

playerTurn = self.player.hprInterval(.2, Point3((newH+180)%360.0, 0, 0))

PS : I added .1 second to the player turning duration, I thought .1 sec is a little fast, if compared to his walking speed.

Addition :

  1. in stopWalkAnim(), “self.playerMove = None”
    The involved interval object is actually self.playerMovement.
  2. self.playerMovement.setDoneEvent(“player-stopped”)
    It generates event when it’s done, but in this case :
    [1] user clicks on point1 and Ralph will walk there
    [2] before Ralph reaches point1 (remaining current interval time : say point1time ), user click on point2, and the new interval will start
    [3] during his way to point2 since the 2nd user-click, in the next point1time, the doneEvent will be generated, so the animation will stop while Ralph is on his way to point2 (he poses and slides).
    To fix this, you must remove the doneEvent before defining the new interval, so the last interval will have no doneEvent, which will avoid stopping the anim on the middle way.
        # setting off the done event
        if self.playerMovement:
           self.playerMovement.setDoneEvent("")
        # the new interval
        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)

Jo you are amazing! You are incredibly good at this :smiley:. I followed your advice and replaced my old playerTurn function:

playerTurn = self.player.hprInterval(.1, Point3(newH, newHpr[1], newHpr[2]))

With your new playerTurn function:

playerTurn = self.player.hprInterval(.2, Point3((newH+180)%360.0, 0, 0))

It’s wonderful! My only problem with it is that my old enemy ‘overturning/wrapping’ (where every now and then the player spins a full circle in the opposite direction) has returned :frowning:. That’s what the ‘newH, newHpr[1], newHpr[2]’ was supposed to stop from happening.

So I tried adding the following, but it didn’t seem to help (and of course, Ralph started leaning over again :confused:):

playerTurn = self.player.hprInterval(.2, Point3((newH+180), newHpr[1], newHpr[2]))

Any advice or suggestions on how to fix this would be very welcome.

My second problem. You are absolutely right about the second click messing up the first (I hadn’t even noticed this, so thanks very much for catching this) I replaced my stopWalkAnim function:

def stopWalkAnim(self):
        # This is called when the movement animation has finished.
        # We can then stop the walk animation.
        self.player.stop("walk")
        self.player.pose("walk",17)
        self.playerMove = None 

with this:

  def stopWalkAnim(self):
        # This is called when the movement animation has finished.
        # We can then stop the walk animation.
        # setting off the done event
        if self.playerMovement:
           self.playerMovement.setDoneEvent("")
        # the new interval
        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)

But when I ran it I got this error:

File "C:\scripts\mousewalkercollision7.py", line 240, in stopWalkAnim
self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)
NameError: global name 'playerTurn' is not defined
Script terminated.

So clearly I’ve done something wrong again :blush:. I know I’m a pain, but I can’t tell you how thankful I am for your help with this. Once this is working properly I’ll post it in the Code Snippets forum so that anybody can use it.

Thanks heaps.

Tiptoe, I didn’t mention to replace the stopWalkAnim() code, did I ?
Instead, you just need to remove the doneEvent before defining the new interval (in moveToPosition()) :
The original code :

        # Put both animations into a sequence
        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()

the new code :

        # Put both animations into a sequence
        # removing the last doneEvent
        if self.playerMovement:
           self.playerMovement.setDoneEvent("")

        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()

Chill out, it seems you were too excited so that you did such failure… :open_mouth:

Hehehe don’t be too hard on me Jo, this is very exciting stuff :smiley:. I’m so pleased with this script that I get carried away sometimes :wink:.

Well, everything is working now, except for that silly ‘spin/wrap-around’ bug. It only happens occasionally and it’s very annoying because I have absolutely no idea how to stop it.

I had this problem once before when I first started with this script, at that time I was only using a flat environment model, but the very patient Russ was able to solve it for me. Here is the thread if you’re curious: discourse.panda3d.org/viewtopic … ight=mouse

Do you have any idea’s on what I need to do to stop this bug from happening?

As always, thanks heaps for all your help.