I Need Some Help With My Player Collision Script

It’s not a bug at all, if you really know what EXACTLY it does.
My apology. It’s my fault.
You see, I added the 180 deg (to flip Ralph) directly to the destination Heading when declaring the interval.
In order to calculate the correct closest-destination-angle, the 180 deg must be put into account when calculating it.
So, fix it this way :

        currH = self.player.getH() 
        npH = self.npLook.getH() 
        # add 180 degrees to flip Ralph
        newH = closestDestAngle(currH, npH+180.0)
        # Create a turn animation from current hpr to the calculated new hpr.
        playerTurn = self.player.hprInterval(.2, Point3(newH, 0, 0))

Don’t get carried away this time. :slight_smile:

Brace yourself Jo, I’m gonna get carried away again :wink:. You’ve done it!!! You’ve fixed it!!! If you were here I’d give you such a big kiss!!!

This is just wonderful!!! I can’t thankyou enough for this. I just have one last teeny tiny question (I promise it’s the last, then I’ll leave you in peace :unamused:).

When I set Ralph’s startPos back to (0,0,0) he now starts the game buried up to his eyeballs in the terrain.

How can I position him so that he starts with his feet on the ground (without messing up his collision detection)?

I tried adjusting his startPos Z coordinate, but it messed up his collision detection (as we’ve seen) so I tried editing my terrain model by hand to add a start position to it, but it seemed to have the same effect as changing Ralph’s startPos (ie…it messed up his collision detection).

Once again, thankyou very, very much for all your help.

The easiest way to solve it is creating a simple object to mark his startpos, just like the one in the tutorial: collision detection. The startpos of the ball is marked with an object. You can create as many objects to mark any position you’d like to put him on. You should name those marking objects as their purposes, e.g. startLevel1, startLevel2, startLevel3, etc.
Just keep 1 thing in mind, if you export those object without saving the transformation, to get the position you should use “nodepath.getBounds().getCenter()”, otherwise use the common “nodepath.getPos()”.
Please be careful with your player nodepaths, now it is different from your very original ones. Now, the self.player_dummy_node is always on XY plane (Z=0), and self.player is the one which wandering along Z axis.

Good luck. :slight_smile:

Edit: Oops, I messed up again. Changing the player_dummy_node’s position didn’t fix the problem. So I followed Jo’s advice and added the following code to the end of my 3ds Max terrain model:

<Group> start_point {
  <DCS> { net }
  <Transform> {
    <Matrix4> {
      1 0 0 0
      0 1 0 0
      0 0 1 0
      0.0 0.0 1.7 1
    }
    <Translate> { 0 0 0 }
  }
}

Now it works!!! Thanks heaps Jo.

Well here it is, the completed code. I’ve added lots of extra comments to it and tried to make it as clear as possible. If anybody has any suggestions for improvements or changes that you think should be made, please post them.

If you think it’s OK, then I’ll add it to the Code Snippets forum :smiley:.

# 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.
# We need to import this function for the player's rotation to work properly.
from direct.showbase.PythonUtil import closestDestAngle 
import sys

class Controls(DirectObject):
    def __init__(self):
        base.disableMouse() # Disable default camera.
        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's 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)
        # Load an object that we can collide into.
        self.box = loader.loadModel("models/box2")
        self.box.reparentTo(render)
        self.box.setPos(25, -25, 1.5)
        self.box.setScale(.3)
        # Find the collision tag in the box model. The following tag was added
        # by hand: <Group> Box01 {
        # <Collide> Box01 { Sphere keep descend }
        self.colObject = self.box.find("**/Box01")
        # Set its collision bitMask to (1). Now only objects whose bitMask is
        # also (1) can collide with the box.
        self.colObject.node().setIntoCollideMask(BitMask32.bit(1))
        # Uncomment the next line to see the collision sphere.
        # self.colObject.show()
        
        # For the camera to rotate independently of the player a 'player dummy 
        # node' and a 'camera dummy node' need to be created. Both dummy nodes 
        # are then 'parented' to the 'player dummy node' making them "siblings" 
        # under the player dummy node. This means that any transformations
        # performed on the dummy node will be inherited by the player model and
        # the camera. Moving the player dummy node will move both the player 
        # model and the camera, but moving or rotating the player model itself 
        # won't effect the camera (because the camera isn't directly parented 
        # to it). 
        
        # Create the player's dummy node.
        self.player_dummy_node = render.attachNewNode("player_dummy_node")
        # Position the dummy node.
        self.player_dummy_node.setPos(0, 0, 0)
        self.player_dummy_node.setHpr(0, 0, 0)
        # The terrain model was edited by hand to include a start position for
        # the player. Use the Find command to locate it.
        self.playerStart = self.environ.find("**/start_point").getPos()
        # Now load the player model and its animations.
        self.player = Actor.Actor("models/ralph",{"walk":"models/ralph-walk"})
        # Set the player to the start position.
        self.player.setPos(self.playerStart)
        # Attach/parent the player model to the player dummy node.
        self.player.reparentTo(self.player_dummy_node)
        # The player model is too large, so scale it down by 50%.
        self.player.setScale(.5)
        #Now create the camera dummy node.
        self.camera_dummy_node = render.attachNewNode("camera_dummy_node")
        # Attach/parent the camera dummy node to the player dummy node.
        self.camera_dummy_node.reparentTo(self.player_dummy_node)
        # Attach/parent the main camera to the camera dummy node.
        camera.reparentTo(self.camera_dummy_node)
        # Position the main 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). Now only objects that 
        # have their From bitmask also set to (0) can collide with the terrain.
        self.ground.node().setIntoCollideMask(BitMask32.bit(0))
        # Create a CollisionTraverser for the picker ray. 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) such as the terrain.
        self.pickerNode.setFromCollideMask(BitMask32.bit(0))
        # Set the collision node's Into collide mask to allOff so that nothing
        # can collide into the ray.
        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.
        # We want the player's collision ray to collide with both the ground
        # and static objects in the world such as rocks and trees. The ground 
        # has its collision bitMask set to (0) and static objects in the world 
        # have their bitMasks set to (1). The player's ray needs to collide 
        # with both, so it needs to have two 'From' collision bitMasks assigned.
        # We do this by creating a special collision mask.
        self.mask = BitMask32().allOff() # Turn off all collisions.
        # Now set the masks collision bitMasks to both (0) and (1).
        self.mask.setBit(0)
        self.mask.setBit(1)
        # 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_dummy_node.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. Now the ray can collide
        # with objects that have both bitMask(0) and bitMask(1).
        self.groundCol.setFromCollideMask(self.mask)
        # 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 and register it as something
        # that can cause collisions with the traverser.
        self.floorHandler.addCollider(self.groundColNp, self.player_dummy_node)
        # Make a CollisionTraverser for the correctPlayerZ function.
        self.Zcoll = CollisionTraverser()
        # Make a handler for the correctPlayerZ function.
        self.ZcollQueue = CollisionHandlerQueue()
        # Register it as something that can cause collisions with the traverser.
        self.Zcoll.addCollider(self.groundColNp, self.ZcollQueue)
        
        # Uncomment this line to see the collisions
        # self.Zcoll.showCollisions(render)

        # Uncomment this line to see the collision rays
        # self.groundColNp.show()
        # 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()+(.8*dir)))
        self.camZoom.start()
        # End cameraZoom
    
    # Define a function to correct the player's Z axis so that he follows the
    # contours of the ground.
    def correctPlayerZ(self, time):
        startpos = self.player.getPos()
        # Check for collisions
        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

    # Define a function to get the position of the mouse click on the terrain.
    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)
            # Set the Z axis to match the ground.
            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())
        # Create a dummy node.
        self.npLook = render.attachNewNode("npLook")
        # Calculate its position.
        self.npLook.setPos(self.player.getPos(render))
        # Make it look at the clicked position.
        self.npLook.lookAt(self.position)
        # Prevent overturning or 'wrap-around' by adjusting the player's heading
        # by 360 degrees.
        reducedH = self.player.getH()%360.0
        # Set the player's heading to this value.
        self.player.setH(reducedH)
        # Get the player's new heading.
        currH = self.player.getH()
        # Get the dummy node's heading.
        npH = self.npLook.getH()
        # Ralph was modeled facing backwards so we need to add 180 degrees to 
        # stop him walking backwards. If your model is not modeled backwards 
        # then delete the + 180.0.
        newH = closestDestAngle(currH, npH + 180.0)
        # Create a turn animation from current heading to the calculated new heading.
        playerTurn = self.player.hprInterval(.2, Point3(newH, 0, 0))
        # 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 axis as we go along.
        # So that the player stays on the ground.
        playerPositionZ = LerpFunc(self.correctPlayerZ, duration=(distance / self.movementSpeed))
        
        # Put the animations into a sequence and set the 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()
        # 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()

What do you think? Are there any corrections or changes that need to be made?

Edit: Since posting my last code I’ve made a few changes to it. I’ve added a box to the scene as something for Ralph to collide into. And I changed the player’s collision ray to collide with both the box and the ground. It seems to be working, the collision is being detected, but Ralph is walking over the top of the box instead of going around it.

It’s very, very late and I’m going to bed. So I’ll play with this some more tomorrow.

Thanks everybody.

I think it’s a great contribution, you took the time to finish this (and kudos to ynjh_jo to help with the details) and comment it with such detail - this is a great reference for a lot of people, especially regarding what you can do with Intervals. Good work.

Cheers,
kaweh

Tiptoe, you missed 1 thing. It’s not critical, but it’s still there.
Check the stopWalkAnim(), it is “self.playerMove = None”, doesn’t it suppose to be “self.playerMovement = None” ?
Pay attention to the details.

About making the player go around the box, you can try CollisionHandlerPusher, with it’s challenges, or instead using path-finding approaches -> https://discourse.panda3d.org/viewtopic.php?t=1061
It means messing around with your code … again.
And if you may want to switch back to CollisionHandlerFloor, read : https://discourse.panda3d.org/viewtopic.php?t=2054
But if you don’t want to go back there, remove any code related to it, there are several lines from your very old code.

Did you hand-edit your egg just to put the marker ?
Can’t you create it from Max ? I haven’t tried Max, so I don’t know.
If you can, then it’s easy to create several startpoint, i.e. to randomize the player startpos if prefered, or for other purposes.

Hiya Jo, I’ve corrected stopWalkAnim() as you advised. I also agree with you about the box collision code (I’ll leave that for another day) so I’ve removed all reference to it.

As for adding collision tags to models, I haven’t seen anyway to do it with the Max exporter, so I’m just adding them by hand. You’re absolutely right that it would be much better to add them when the model is created, but it’s not too much trouble to add them by hand, so I can live with it :unamused:.

Well that’s it, all done!!! I’m off to post this code to the ‘Code Snippets’ forum :smiley:. I really can’t thankyou enough for all your help with this.

The Panda3D community is the best!!!

Cheers

What was chasing your back ? Don’t be so rush ! It’s not done yet !
There are some flaws remain in the code.

  1. critical :
    Sorry, in correctPlayerZ():
if self.ZcollQueue.getNumEntries>0:

I missed the :open_mouth: brackets :open_mouth: !
getNumEntries → getNumEntriesb[/b]
:laughing: plz laugh…

  1. In the code snippet, you should mention the reason for Why the “player” doesn’t move with “player_dummy_node” altogether, so that it will never confuse people.
    It’s because the nature of posInterval, which has start and end position, so the inbetween position is calculated using the start and end position, based on the current time. (-> correct me if this is wrong !!!)
    The possible way to solve this is set the “player_dummy_node” to move only on XY plane, and the Zcorrection is performed on the “player”.
    If the Zcorrection is performed on the “player_dummy_node”, and when the CollisionRay’s origin is lower than the ground, there won’t be any collision, so the player will be on the posInterval track !

  2. very very CRITICAL & embarrassing !!
    Pause the parallel and remove the self.playerMovement reference before declaring the new one.

        if self.playerMovement:
           # due to pausing the parallel, we don't need removing the doneEvent anymore
#           self.playerMovement.setDoneEvent("")
           self.playerMovement.pause()
           self.playerMovement = None

The problem WITHOUT doing this is :
the previous/unfinished intervals will be keep updated, eventhough you’ve set it to None (but without pausing the intervals) !
To prove it, you only need 2 clicks :
[1] 1st click : far away from the player (so that the 1st duration is longer than the 2nd one)
[2] 2nd click : so close to the player (so that the 2nd duration is as minimum as possible)
[3] wait & see
The player will keep moving, finishing the 1st interval !!

There is a method “clearIntervals”, it gives the same result as using “pause”.

        if self.playerMovement:
           self.playerMovement.setDoneEvent("")
           self.playerMovement.clearIntervals()
           self.playerMovement = None

Yes, but what lies behind them is not the same !
Print the intervalManager after declaring the new parallel :

        self.playerMovement = Parallel(playerTurn, playerMove, playerPositionZ)
        self.playerMovement.setDoneEvent("player-stopped")
        self.player.loop("walk")
        self.playerMovement.start()
        # print the intervals actually handled by intervalManager
        print self.playerMovement.getManager()

Run it and check it this way : click rapidly as many time as possible around the player.

Compare the parallel(s) handled by the manager for those 2 different approaches.
[1] using “pause”
only 1 parallel is handled at a time
[2] using “clearIntervals”
several parallels remain there, (I guess) until all of them finished (full duration).

Don’t hesitate to test any code, moreover before publishing it !
Don’t feel satisfied before it can pass any possible ugly circumstances.

Keep fighting !
Beta testing is important !

I meant the startpos marker object, did you hand-edit your egg to put it there ? Why didn’t you create it from Max ? The object can be as simple as a box.
BTW, you don’t need to hand-edit your egg just to put the tag. You can set the collision mask on-the-fly on nodepath level, by calling :

self.ground.setCollideMask(BitMask32.bit(0))

instead of :

self.ground.node().setIntoCollideMask(BitMask32.bit(0))

I’ve used it since the beginning to run your script, and the player never climb up the walls. The terrain and wall objects are separated, and the collision is set only for terrain, so when I click on the wall, the player stays on the last position, or finishing the current interval.

:open_mouth: Wow, your code is a mess. It’s Python, but it looks like it’s written in C. Python is much more elegent than that. :wink:

What a noble idea! :smiley: But dispite all your comments, code as convoluted as yours is difficult to expand and maintain. Python can be object-oriented if you want it to be. (And you do, for any script this long, believe me.) I tried to clean out the garbage and break it up into classes. Sorry it took me a few days, but it really was a mess!

This script requires my keybindings module.

from pandac import PandaModules as P
from direct.actor import Actor
import direct.directbase.DirectStart # Start Panda
from direct.showbase.DirectObject import DirectObject # To listen for 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.fsm import FSM
from keybindings import Controls

base.cTrav = P.CollisionTraverser()
base.cTrav.setRespectPrevTransform(1)
Pusher = P.CollisionHandlerPusher()
Floor = P.CollisionHandlerFloor()
Queue = P.CollisionHandlerQueue()

def fromCol(parent,handler,type,mask = P.BitMask32.allOn()):
        """setup collision solid."""
        nodepath = parent.attachNewNode(P.CollisionNode('frmcol'))
        nodepath.node().addSolid(type)
        nodepath.node().setFromCollideMask(mask)
        nodepath.setCollideMask(P.BitMask32.allOff())
        nodepath.show()#makes the collision solid visible.
        base.cTrav.addCollider(nodepath,handler)
        try:#doesn't work on queues.
            handler.addCollider(nodepath,parent)
        except:
            pass#Don't care. This method needs to work on queues too.
class Avatar(FSM.FSM):
    """Setup ralph."""
    def __init__(self):
        FSM.FSM.__init__(self,'avatar')
        self.prime=P.NodePath("ralph")
        self.myActor=Actor.Actor("models/ralph",
            {"run":"models/ralph-run",
            "walk":"models/ralph-walk"})
        self.myActor.setScale(.2)
        self.myActor.reparentTo(self.prime)
        self.prime.reparentTo(render)
        self.prime.setZ(5)
        #ralph will have collision solids,
        #so the visible geometry shouldn't collide at all
        self.prime.setCollideMask(P.BitMask32.allOff())
        fromCol(self.prime,Floor,P.CollisionRay(0, 0, .4, 0, 0, -1))
        #ideally the ground and walls should have separate collision masks
        #otherwise Pusher can interfere with Floor if the ground gets too steep
        fromCol(self.prime,Pusher,P.CollisionSphere(0,0,.7,.4))
        ##
        self.speed = 3 #moving speed
        self.point = P.Point3.zero()
        self.vel = P.Vec3.zero()
    def update(self,dt):
        self.prime.setFluidPos(#fluidly update position based on velocity
            self.prime.getX()+self.vel.getX()*dt,
            self.prime.getY()+self.vel.getY()*dt,
            self.prime.getZ() )
        #recalc velocity to point to destination
        self.vel = self.point-self.prime.getPos()
        if self.vel.lengthSquared()<.1:
            self.request('Stand')
            self.vel=P.Vec3.zero()#stop moving if you are there
        else:
            self.vel.normalize()
            self.vel*=self.speed
    def setDestination(self,point):
        self.point = point
        pr = self.prime.getP(),self.prime.getR()
        self.prime.lookAt(self.point)
        self.prime.setHpr(self.prime.getH()-180,*pr)
        self.vel = self.point-self.prime.getPos()
        self.vel.normalize()
        self.vel*=self.speed
        self.request('Run')
    def enterRun(self):
        self.myActor.loop("run")
    def exitRun(self):
        self.myActor.stop("run")
    def enterStand(self):
        self.myActor.pose("walk",17)
#end Avatar
class Environ:
    """Setup Environ"""
    def __init__(self):
        self.prime = loader.loadModel("models/world")
        self.prime.reparentTo(render)
        #ideally the ground and walls should have separate collision masks
        #otherwise Pusher can interfere with Floor if the ground gets too steep
        self.prime.setCollideMask(P.BitMask32.allOn())
class Marker:
    def __init__(self):
        #you probably want to use a different marker model
        self.prime = loader.loadModel('jack')
        self.prime.reparentTo(render)
        self.prime.setScale(.1,.1,.1)
        #this is just a display element, so it shouldn't affect the world.
        self.prime.setCollideMask(P.BitMask32.allOff())
class EdgeScreenTracker(DirectObject):
    def __init__(self,avatar,speed=.10):
        base.disableMouse() # Disable default camera.
        self.speed = speed # Controls speed of camera rotation and zoom.
        self.accept('zoom in', self.cameraZoom,[-1])
        self.accept('zoom out', self.cameraZoom,[1])
        self.avatar = avatar
        self.loadCamera()
        taskMgr.add(self.mousecamTask, "mousecamTask")
    def mousecamTask(self,task):
        """Rotate camera when the pointer moves to the edges of the screen."""
        base.camera.lookAt(self.avatar)# Make the camera follow the player.
        # 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:
            LerpHprInterval(self.dum, self.speed,
                Point3(self.dum.getH()-10,self.dum.getP(),self.dum.getR(), ),
                ).start()
        elif mpos.getX() < -0.99:
            LerpHprInterval(self.dum, self.speed,
                Point3(self.dum.getH()+10,self.dum.getP(),self.dum.getR(), ),
                ).start()
        if mpos.getY() > 0.9:
            LerpHprInterval(self.dum, self.speed,
                Point3(self.dum.getH(),self.dum.getP()+1,self.dum.getR(), ),
                ).start()
        elif mpos.getY() < -0.9:
            LerpHprInterval(self.dum, self.speed,
                Point3(self.dum.getH(),self.dum.getP()-1,self.dum.getR(), ),
                ).start()
        return Task.cont
    def loadCamera(self):
        #Create a camera dummy node
        self.dum = render.attachNewNode("dum")
        self.dum.reparentTo(self.avatar)
        #don't rotate the dummy with the avatar
        self.dum.node().setEffect(CompassEffect.make(render))
        # Attach the camera to the dummy node.
        base.camera.reparentTo(self.dum)
        # Position the camera
        base.camera.setPos(0, -30, 20)
    def cameraZoom(self,dir):
        """Define the cameraZoom function.

        This needs some work! "zoom in" can go past the avatar!"""
        LerpPosInterval(
            camera, self.speed,
            Point3(camera.getX(),camera.getY()-(2*dir),camera.getZ()),
            ).start()
#end EdgeScreenTracker
class World(DirectObject):
    def __init__(self):
        self.avatar = Avatar()#setup ralph
        self.ralph = self.avatar.prime
        self.environ = Environ().prime#load environ
        EdgeScreenTracker(self.ralph)#setup camera
        #setup keyboard
        self.conrols = Controls(translation={
            'zoom in':['wheel_down','arrow_up'],
            'zoom out':['wheel_up','arrow_down'],
            'click':['mouse1','mouse3']})
        #for movement
        self.last = 0
        taskMgr.add(self.gameLoop, "gameLoop")#movement task
        #for picking
        self.pickerRay = CollisionRay()
        #ideally the ground and walls should have separate collision masks
        #so the picker ray can be set to only collide into ground, but the
        #world model has walls (and rocks and trees) already baked in.
        fromCol(camera,Queue,self.pickerRay)
        self.accept('click',self.OnClick)
        self.marker = Marker().prime
        #uncomment this line to show the collisions
        base.cTrav.showCollisions(render)
    def OnClick(self):
        #the screen coordinates of the mouse
        mpos=base.mouseWatcherNode.getMouse()
        #This makes the ray's origin the camera and makes the ray point
        #to the screen coordinates of the mouse
        self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
        #wait a few frames and move
        taskMgr.doMethodLater(.02,self.setDestination,'setDest')
    def setDestination(self,task):
        #find the position of the nearest intersection in renderspace
        if Queue.getNumEntries() > 0:
            Queue.sortEntries() #this is so we get the closest object
            self.point=Queue.getEntry(0).getSurfacePoint(render)
        self.marker.setPos(self.point)
        self.avatar.setDestination(self.point)
    def gameLoop(self,task):
        dt = task.time - self.last#time elapsed since the last frame.
        self.last = task.time
        self.avatar.update(dt)
        return direct.task.Task.cont
#end World
World()
run()

Oh Wow! Thankyou very, very much Cyan. I knew there had to be a better way than mine :unamused:. Sad to say it, but I really am very bad at programming (though finger’s crossed I think I’m getting better).

Jo, please forgive me, I was so thrilled to get this code working that I kinda jumped the gun a bit, to my inexperienced eyes I thought it was finished :unamused:. Do you think I should delete it from the Code Snippets forum?

Ah well, even with all its faults, I can’t tell you how pleased I am with this script. It works!! And I’ve never managed to get this far with any other engine before.

This really says a lot for the power of Panda and Python (and this terrific community). Because I really am a total beginner when it comes to programming (a few months ago I didn’t even know what a variable was).

Oh! One more thing, when I tried using the world model from “Roaming Ralph” he walked up the walls. Are the ground and walls modeled seperately? Is it possible to set a different bitMask on the walls so that the player’s ray won’t collide with them?

Again, thanks heaps guys (without you, I’d be totally lost :smiley: ).

It’s not true. The world consists of several parts : terrain, wall, rocks, and trees.

→ Tiptoe :
I’ve used the world of Ralph as you’ve used your own, by finding the terrain model and set the collision mask. The terrain in Ralph’s world is named “terrain”.

So, it’s obviously possible to manage the collision handling into 2 different masks. I’ve tried it,
by changing the initialization of Environ class :

        self.prime = loader.loadModel("models/world")
        self.prime.reparentTo(render)
        self.terrain=self.prime.find("**/terrain")
        # set the collision mask of the whole world to "0"
        self.prime.setCollideMask(P.BitMask32.bit(0))
        # set the collision mask of the terrain to "1"
        self.terrain.setCollideMask(P.BitMask32.bit(1))

and changing 4 separated lines :

def fromCol(parent,handler,type,mask):
.
.
        # to collide with the terrain which has mask "1"
        fromCol(self.prime,Floor,P.CollisionRay(0, 0, .4, 0, 0, -1),P.BitMask32.bit(1))
.
.
        # to collide with the other things in the world which have mask "0"
        fromCol(self.prime,Pusher,P.CollisionSphere(0,0,.7,.4),P.BitMask32.bit(0))   
.
.
        # to collide with the terrain which has mask "1"
        fromCol(camera,Queue,self.pickerRay,P.BitMask32.bit(1))

IMO, the more codes, the more things to learn from, both the traditional the advanced ones, as long as they’re working codes (bugs-free). Moreover, we learn the same thing : Panda3D’s capabilities, which can be learned in anyway, based on the people’s level.

Excellent ynjh_jo! Here’s the updated code with more comments and a few organizational improvements.

from pandac import PandaModules as P #alias PandaModules as P
from direct.actor import Actor
import direct.directbase.DirectStart # Start Panda
from direct.showbase.DirectObject import DirectObject # To listen for Events
from direct.task import Task # To use Tasks
from direct.actor import Actor # To use animated Actors
from direct.interval import LerpInterval as LERP#alias LerpInterval as LERP
from direct.fsm import FSM
from keybindings import Controls

base.cTrav = P.CollisionTraverser()#initialize traverser
#collision detection can fail without fluid movement. (Quantum tunneling)
base.cTrav.setRespectPrevTransform(1)
Pusher = P.CollisionHandlerPusher()#pusher keeps its ward out of things
Floor = P.CollisionHandlerFloor()#floor keeps its ward grounded
Queue = P.CollisionHandlerQueue()#no ward to speak of.
#setup keyboard and mouse
con = Controls(translation={
    'zoom in':['wheel_down','arrow_up'],
    'zoom out':['wheel_up','arrow_down'],
    'click':['mouse1','mouse3']})
def fromCol(parent,handler,type,mask = P.BitMask32.allOn()):
        """setup collision solid."""
        nodepath = parent.attachNewNode(P.CollisionNode('frmcol'))#attach
        nodepath.node().addSolid(type)#add the solid
        nodepath.node().setFromCollideMask(mask)#allow selective masking
        nodepath.setCollideMask(P.BitMask32.allOff())#it's a from solid only.
        ##uncomment this line to make the collision solid visible:
        #nodepath.show()
        base.cTrav.addCollider(nodepath,handler)#add to traverser
        try:#doesn't work on queues.
            handler.addCollider(nodepath,parent)#keep the ward out of trouble
        except:
            pass#Don't care. This method needs to work on queues too.
class Avatar(FSM.FSM):
    """Setup ralph."""
    def __init__(self):
        FSM.FSM.__init__(self,'avatar')#you must call the base init
        self.prime=P.NodePath("ralph")
        self.myActor=Actor.Actor("models/ralph",#the model
            {"run":"models/ralph-run",#the animations
            "walk":"models/ralph-walk"})
        self.myActor.setH(180)#ralph's Y is backward.
        self.myActor.setScale(.2)#scale actor, not prime.
        self.myActor.reparentTo(self.prime)
        self.prime.reparentTo(render)
        self.prime.setZ(5)#be sure to start above the floor.
        #ralph will have collision solids, so the
        # visible geometry shouldn't collide at all
        self.prime.setCollideMask(P.BitMask32.allOff())
        # to collide with the terrain which has mask "1" & keep ralph grounded
        fromCol(self.prime,Floor,
            P.CollisionRay(0, 0, 1.3, 0, 0, -1),P.BitMask32.bit(1)) 
        fromCol(self.prime,Floor,P.CollisionRay(0, 0, 1.3, 0, 0, -1))
        #The ground and walls should have separate collision masks otherwise
        # Pusher can interfere with Floor if the ground gets too steep.
        #The sphere is set to collide with the other things in
        # the world which have mask "0"
        fromCol(self.prime,Pusher,
            P.CollisionSphere(0,0,.7,.4),P.BitMask32.bit(0))
        #initialize movement variables
        self.speed = 3 #moving speed
        self.point = P.Point3.zero()#destination
        self.vel = P.Vec3.zero()#velocity
    def update(self,dt):
        self.prime.setFluidPos(#fluidly update position based on velocity
            self.prime.getX()+self.vel.getX()*dt,
            self.prime.getY()+self.vel.getY()*dt,
            self.prime.getZ() )
        #recalc velocity to point to destination
        self.vel = self.point-self.prime.getPos()
        if self.vel.lengthSquared()<.1:
            self.request('Stand')#change FSM state
            self.vel=P.Vec3.zero()#stop moving if you are there
        else:
            self.vel.normalize()#set magnitude to 1
            self.vel*=self.speed#the magnitude of velocity is speed.
    def setDestination(self,point):
        self.point = point
        pr = self.prime.getP(),self.prime.getR()#to preserve pitch and roll
        self.prime.lookAt(self.point)#lookAt affects all three (HPR)
        #keep heading but revert pitch and roll.
        self.prime.setHpr(self.prime.getH(),*pr)
        #subtracting points yeilds a vector pointing from the 1st to the 2nd
        self.vel = self.point-self.prime.getPos()#destination from position
        self.vel.normalize()#set magnitude to 1
        self.vel*=self.speed#the magnitude of velocity is speed.
        self.request('Run')#change FSM state
## State handlers. Only define them if something happens at that transition.
    def enterRun(self):
        self.myActor.loop("run")#loop the run animation
    def exitRun(self):
        self.myActor.stop("run")#stop the run animation
    def enterStand(self):
        self.myActor.pose("walk",17)#both feet on the floor
    #notice that no def exitStand is required.
#end Avatar
class Environ:
    """Setup Environ.

    At the moment this simply loads a model an activates its collisions"""
    def __init__(self):
        self.prime = loader.loadModel("models/world")
        self.prime.reparentTo(render)
        self.terrain=self.prime.find("**/terrain")
        # set the collision mask of the whole world to "0"
        self.prime.setCollideMask(P.BitMask32.bit(0))
        # set the collision mask of the terrain to "1"
        self.terrain.setCollideMask(P.BitMask32.bit(1))
#end Environ
class Marker:
    """Setup marker to mark the clicked destination.

    At the moment simply loads jack and turns off his collisions."""
    def __init__(self):
        #you probably want to use a different marker model
        self.prime = loader.loadModel('jack')
        self.prime.reparentTo(render)
        self.prime.setScale(.1,.1,.1)
        #this is just a display element, so it shouldn't affect the world.
        self.prime.setCollideMask(P.BitMask32.allOff())
#end Marker
class EdgeScreenTracker(DirectObject):
    """Mouse camera control interface."""
    def __init__(self,avatar,speed=.10):
        base.disableMouse() # Disable default camera interface.
        self.speed = speed # Controls speed of camera rotation and zoom.
        self.accept('zoom in', self.cameraZoom,[-1])#translated.
        self.accept('zoom out', self.cameraZoom,[1])
        self.avatar = avatar#what to point the camera at
        self.loadCamera()
        taskMgr.add(self.mousecamTask, "mousecamTask")
        base.camera.lookAt(self.avatar)# Make the camera follow the player.
    def mousecamTask(self,task):
        """Rotate camera when the pointer moves to the edges of the screen."""
        if not base.mouseWatcherNode.hasMouse():#See if the mouse is available
            return Task.cont#if no, just loop again
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH()-10,self.dum.getP(),self.dum.getR(), ),
                ).start()#this is a single logical line
        elif mpos.getX() < -0.99:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH()+10,self.dum.getP(),self.dum.getR(), ),
                ).start()
        if mpos.getY() > 0.9:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH(),self.dum.getP()+3,self.dum.getR(), ),
                ).start()
        elif mpos.getY() < -0.9:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH(),self.dum.getP()-3,self.dum.getR(), ),
                ).start()
        return Task.cont
    def loadCamera(self):
        self.dum = render.attachNewNode("dum")#Create a camera dummy node
        self.dum.reparentTo(self.avatar)
        #don't rotate the dummy with the avatar
        self.dum.node().setEffect(P.CompassEffect.make(render))
        base.camera.reparentTo(self.dum)# Attach the camera to the dummy node.
        base.camera.setPos(0, -30, 20)# Position the camera
    def cameraZoom(self,dir):
        """Define the cameraZoom function.

        This needs some work! "zoom in" can go past the avatar!"""
        LERP.LerpPosInterval(
            camera, self.speed,
            P.Point3(camera.getX(),camera.getY()-(2*dir),camera.getZ()),
            ).start()
#end EdgeScreenTracker
class World(DirectObject):
    def __init__(self):
        self.avatar = Avatar()#setup ralph
        self.environ = Environ().prime#load environ
        self.marker = Marker().prime#load marker
        EdgeScreenTracker(self.avatar.prime)#setup camera
        self.last = 0#for movement in gameLoop
        taskMgr.add(self.gameLoop, "gameLoop")#movement task
        self.pickerRay = P.CollisionRay()#for picking
        # to collide with the terrain which has mask "1"
        fromCol(camera,Queue,self.pickerRay,P.BitMask32.bit(1))
        self.accept('click',self.OnClick)#translated
        ##uncomment this line to show the collisions:
        #base.cTrav.showCollisions(render)
    def OnClick(self):
        mpos=base.mouseWatcherNode.getMouse()#mouse's screen coordinates 
        #This makes the ray's origin the camera and makes the ray point to mpos
        self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
        #wait a few frames and move
        taskMgr.doMethodLater(.02,self.setDestination,'setDest')
    def setDestination(self,task):
        #find the position of the nearest intersection in renderspace
        if Queue.getNumEntries() > 0:
            Queue.sortEntries() #this is so we get the closest object
            self.point=Queue.getEntry(0).getSurfacePoint(render)
        self.marker.setPos(self.point)#marker indicates destination
        self.avatar.setDestination(self.point)#set the destination
    def gameLoop(self,task):
        dt = task.time - self.last#time elapsed since the last frame.
        self.last = task.time#for calculating next time
        self.avatar.update(dt)#allow avatar to update itself
        return direct.task.Task.cont#task must return cont to continue a loop
#end World
World()#no need to store to a variable
run()

I think the edge screen tracker still needs some work though. The camera can go through solid objects and underground, and the zoom in can go past the avatar. Any ideas?

I’m not Jo, but I think so. Or at least we should post the new one too when it’s done. It might save us trouble later.

Guy’s this is so brilliant! I love that we can now use the world model from Roaming Ralph. It really bothered me that I couldn’t use the models that came with Panda. This is so much better, now anybody can use it.

Cyan, your code is marvelous! I just wish I had the knowledge to understand half of what you’ve done :unamused:. I’ve really tried to follow it, but my lack of experience is a real handicap, I can’t for the life of me understand how you setup the collision stuff.

In the Roaming Ralph example and my horrible messy code I can see how the collision rays are created and what they’re attached to. But you use a different method that baffles hopeless newbs like me :wink:.

You see, I had an idea about the camera going under the ground. In the Roaming Ralph example they used a task to keep the camera above both the ground and Ralph. So I tried adding it to the mousecamTask like so:

     def mousecamTask(self,task):
        """Rotate camera when the pointer moves to the edges of the screen."""
        if not base.mouseWatcherNode.hasMouse():#See if the mouse is available
            return Task.cont#if no, just loop again
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH()-10,self.dum.getP(),self.dum.getR(), ),
                ).start()#this is a single logical line
        elif mpos.getX() < -0.99:
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH()+10,self.dum.getP(),self.dum.getR(), ),
                ).start()
                
        # Keep the camera at one foot above the terrain,
        # or two feet above ralph, whichever is greater.
        
        entries = []
        for i in range(self.camGroundHandler.getNumEntries()):
            entry = self.camGroundHandler.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"):
            base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+1.0)
        if (base.camera.getZ() < self.myActor.getZ() + 2.0):
            base.camera.setZ(self.myActor.getZ() + 2.0)
            
        # Store the task time and continue.
        self.prevtime = task.time
        return Task.cont

It’s not working of course, because camGroundHandler doesn’t exist, I thought I’d just have to rename it, but I can’t see what you used in its place (in fact, ‘stupid me’ can’t even see how you setup the camera’s collision at all :confused: ).

I told you I was bad at this :unamused:. What do you think? Do you think it might answer?

Thanks heaps Guys.

With Cyan’s code, I played around with these things :

  1. if you click and there is no collision (not colliding into terrain), Ralph will flip instead of not responding. Check your setDestination(), and indent the last 2 lines. What was wrong with your logic ?

  2. You’d better set the thresholds for the camera-pitch-adjustment. You don’t want your avatar looks like a bat :open_mouth: hanging on the ceiling, do you ? :laughing: :laughing: :laughing:
    There must be a Python standard function for value-clamping, I’ve never used it though.
    ATM I’m still have no access to my Python reference, so I created my own, something like this :

def myclamp(val,min,max):
    if val<min:
       val=min
    elif val>max:
       val=max
    return val

I declared it globally (outside any classes, just like fromCol function).
Next, change the pitch adjustment code to :

        if mpos.getY() > 0.9:
            newP=myclamp(self.dum.getP()+3,-80,0)
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH(),newP,self.dum.getR(), ),
                ).start()
        elif mpos.getY() < -0.9:
            newP=myclamp(self.dum.getP()-3,-80,0)
            LERP.LerpHprInterval(self.dum, self.speed,
                P.Point3(self.dum.getH(),newP,self.dum.getR(), ),
                ).start()

The minimum deg (-80) depends on the Z position of your camera, which is set in EdgeScreenTracker.loadCamera().

So, you want to set the zoom origin exactly on the avatar ?
If it’s the case, then it’s simple, because the camera dummy node is parented to the avatar.
I set the zoom origin on the avatar’s head. The camera offset is set in loadCamera.
I implemented the percentage zooming, instead of adding/subtracting by fixed-distance, so it will zoom fast, not 1 footstep at a time. :smiley:
Replace these 2 functions completely :

    def loadCamera(self):
        self.dum = render.attachNewNode("dum")#Create a camera dummy node
        self.dum.reparentTo(self.avatar)
        # shift the camera dummy node to the height of avatar's head
        # to set the zooming origin
        self.dum.setZ(1)
        #don't rotate the dummy with the avatar
        self.dum.node().setEffect(P.CompassEffect.make(render))
        base.camera.reparentTo(self.dum)# Attach the camera to the dummy node.
        base.camera.setPos(0, -30, 5)# Position the camera
    def cameraZoom(self,dir):
        """Define the cameraZoom function."""
        # for fast zoom, we use percentage, instead of sequential fixed-distance
        # the distance of camera-avatar is increased/decreased by zoomPercent
        zoomPercent=.7
        # since the camera is parented to the avatar, we only need to zoom relative to (0,0,0)
        point=P.Point3(camera.getPos())*(1+dir*zoomPercent)

        # minDistance is the minimum distance allowed for the camera to get close to the avatar
        minDistance=5
        if P.Vec3(point).length()>minDistance:
           LERP.LerpPosInterval(
                camera, self.speed,
                P.Point3(point)).start()

There are 3 collisions currently set :
= sphere-world, handled by Pusher, using bit(0)
= ray-terrain, handled by Floor, using bit(1)
= (mouse)ray-terrain, handled by Queue, using bit(0)
All of them is set by function fromCol() to simplify the long process if done 1 by 1, e.g. :

fromCol(self.prime,Floor,P.CollisionRay(0, 0, 1.3, 0, 0, -1),P.BitMask32.bit(1))

it sends self.prime as the parent node (the node where the handler plays with), Floor as the collision handler, collision ray as the collision solid, and bit 1 as the collision bitmask. They’re sent to fromCol() function. In fromCol() function, they’re processed as you can see, the usual steps in setting collision, in this case, setting the “from” collision only.
Generally, a function is made to simplify the workflow of “long and painful” procedural steps, by passing the objects to the function. That’s the power of high-level language.

Cyan, you want to keep the camera from passing through solid objects, but you didn’t do any collision checking ?
And did you forget removing the original line :

        # to collide with the terrain which has mask "1" & keep ralph grounded
        fromCol(self.prime,Floor,
            P.CollisionRay(0, 0, 1.3, 0, 0, -1),P.BitMask32.bit(1))
        fromCol(self.prime,Floor,P.CollisionRay(0, 0, 1.3, 0, 0, -1))

Simply leaving your camera to Pusher is a good idea, but it will cause view jitters. There must be a better solution.

Heh heh, :blush: oops. I’m glad you caught that one. I tried all kinds of things to try to figure out why the setDestination() set the destination to the previous click’s coordinates. It seems that the collision traverser can’t execute until after the OnClick() returns. So I finally settled on a doMethodLater, but I forgot to indent that part again.

To my knowledge there isn’t a two-side clamp method built in. (But what do I know? I’ve only been writing Python since July 8) ) There are however built-in min() and max() functions. You can pass any number of values to them and it will return the min or max. You can also nest them to get your two-side clamp as I will demonstrate:

I cleaned up your new methods to create the new, improved EdgeScreenTracker featuring two-side clamped zoom and pitch.

class EdgeScreenTracker(DirectObject):
    """Mouse camera control interface."""
    def __init__(self,avatar,speed=.10,minDist=5,maxDist=50):
        base.disableMouse() # Disable default camera interface.
        self.speed = speed # Controls speed of camera rotation and zoom.
        self.minDist=minDist # The closets camera can be to avatar.
        self.maxDist=maxDist # The farthest camera can be to avatar.
        self.accept('zoom in', self.cameraZoom,[0.7])#translated.
        self.accept('zoom out', self.cameraZoom,[1.3])
        self.avatar = avatar#this is what to point the camera at.
        self.loadCamera()
        taskMgr.add(self.mousecamTask, "mousecamTask")
    def mousecamTask(self,task):
        """Rotate camera when the pointer moves to the edges of the screen."""
        base.camera.lookAt(self.avatar)# Make the camera follow the player.
        if not base.mouseWatcherNode.hasMouse():#See if the mouse is available.
            return Task.cont#if no, just loop again.
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            self.__rotateCam(P.Point2(-10,0))
        elif mpos.getX() < -0.99:
            self.__rotateCam(P.Point2(10,0))
        if mpos.getY() > 0.9:
                self.__rotateCam(P.Point2(0,-5))
        elif mpos.getY() < -0.9:
            self.__rotateCam(P.Point2(0,3))
        return Task.cont#loop again.
    def __rotateCam(self,arc):
        newP=min(max(self.dum.getP()-arc.getY(),-80),0)#clamp newP
        newH=self.dum.getH()+arc.getX()
        LERP.LerpHprInterval(self.dum, self.speed,
            P.Vec3(newH,newP,self.dum.getR(), ), ).start()
    def loadCamera(self,Pos = P.Point3(0,-30,5)):
        """Load the camera and set it up."""
        self.dum = render.attachNewNode("dum")#Create a camera dummy node
        self.dum.reparentTo(self.avatar)
        self.dum.setZ(2)# shift the camera dummy node up
        #don't rotate the dummy with the avatar
        self.dum.node().setEffect(P.CompassEffect.make(render))
        base.camera.reparentTo(self.dum)# Attach the camera to the dummy node.
        base.camera.setPos(Pos)# Position the camera
    def cameraZoom(self,zoomFactor):
        """Define the cameraZoom function."""
        vec=camera.getPos()*zoomFactor
        newDist=min(max(vec.length(),self.minDist),self.maxDist)#clamp zoom.
        vec.normalize()#set length to 1
        vec*=newDist#set length to clamped value
        LERP.LerpPosInterval(camera,self.speed,vec).start()
#end EdgeScreenTracker

Try http://www.python.org/doc/. That’s what I mostly use.

Yup, forgot.

No, I tried adding a pusher sphere before. Jitter isn’t the only probem. Pusher changes its ward’s position, but the EdgeScreenTracker controls the camera with dum’s rotation. They are incompatible. If the cameras X gets offset changing dum’s pitch starts to affect cameras roll. I think Tiptoe’s idea is closer. Hints to the solution may still lie in the original roaming ralph tutorial script.

In Panda global functions, there are 2 two-sided clamp functions : clamp and clampScalar. Maybe you’d like to use it instead.

Okay, I think I got it working to my satisfaction. Besides the camera work, I added more comments and improved orginization. Check for bugs.

from pandac import PandaModules as P #alias PandaModules as P
import direct.directbase.DirectStart # Start Panda
from direct.showbase.DirectObject import DirectObject # To listen for Events
from direct.task import Task # To use Tasks
from direct.actor import Actor # To use animated Actors
from direct.interval import LerpInterval as LERP#alias LerpInterval as LERP
from direct.fsm import FSM# To use Finite State Machines.
from direct.showbase.PythonUtil import clampScalar#useful.
from keybindings import Controls
base.cTrav = P.CollisionTraverser()#initialize traverser
#Collision detection can fail if objects move too fast. (Quantum tunnelling.)
base.cTrav.setRespectPrevTransform(1)#fluid move prevents quantum tunnelling.
#Global collision handlers.
Pusher = P.CollisionHandlerPusher()#Pusher keeps its ward out of things.
Floor = P.CollisionHandlerFloor()#Floor keeps its ward grounded.
#collision masks. Use a bitwise or (operator | ) if more than one apply.
AllM = P.BitMask32.bit(0)#for everything in the world, except display elements 
GroundM = P.BitMask32.bit(1)#the ground. What the floor rays collide with.
PusherM = P.BitMask32.bit(2)#Solid objects, but not the ground.
CameraM = P.BitMask32.bit(3)#What shouldn't be between camera and avatar.
#setup keyboard and mouse
con = Controls(translation={#initialize global controls
    'zoom in':['wheel_up','arrow_up'],
    'zoom out':['wheel_down','arrow_down'],
    'click':['mouse1','mouse3']})
def fromCol(parent,handler,type,mask = P.BitMask32.allOn()):
        """Setup a from collision solid.
        
        Last I checked CollisionPolygon 's and CollisionTube 's can't be used
        as from solids. If you pass one, it won't hit anything"""
        nodepath = parent.attachNewNode(P.CollisionNode('frmcol'))
        nodepath.node().addSolid(type)#add the solid to the new collisionNode
        nodepath.node().setFromCollideMask(mask)#allow selective masking
        nodepath.setCollideMask(P.BitMask32.allOff())#it's a from solid only.
        ####uncomment this line to make the collision solid visible:
        ##nodepath.show()
        base.cTrav.addCollider(nodepath,handler)#add to the traverser
        try:#the next line doesn't work on queues. (not necessary)
            handler.addCollider(nodepath,parent)#keep the ward out of trouble
        except:
            pass#Don't care. This method needs to work on queues too.
        return nodepath#we might need the new CollisionNode again later.
class Avatar(FSM.FSM):
    """Setup Avatar.
    
    The default values load Ralph, but this can be easily changed."""
    def __init__(self,iniHight=5,dest=P.Point3.zero(),speed=3,act={},solid={}):
        #initialize movement variables
        self.speed = speed #moving speed
        self.point = dest#destination
        self.vel = P.Vec3.zero()#velocity
        #You must call FSM init if you override init.
        FSM.FSM.__init__(self,'avatar')
        #Avatar scenegraph setup.
        self.prime=P.NodePath('avatar prime')#prime: Avatar's primary nodepath.
        self.prime.reparentTo(render)#Make Avatar visible.
        self.prime.setZ(iniHight)#Be sure to start above the floor.
        self.__initActor(**act)#unpacks act dictionary as optional params
        self.__initSolids(**solid)#same with solid
        #default to standing state instead of off.
        self.request('Stand')
    def __initActor(self,model='models/ralph',
            hprs=(180,0,0,.2,.2,.2),#ralph's Y is backward and he's too big.
            anims={"run":"models/ralph-run","walk":"models/ralph-walk"}):
        """Only seperate for organisation, treat it as is part of __init__() .
        
        Load actor and animations. Set coordinate offset from prime."""
        self.myActor=Actor.Actor(model,anims)
        self.myActor.setHprScale(*hprs)
        self.myActor.reparentTo(self.prime)#parent actor to the prime
    def __initSolids(self,ray=(0, 0, 1.3, 0, 0, -1),sphere=(0,0,.7,.4)):
        """Only seperate for organisation, treat it as is part of __init__() .
        
        Set collision solids for the avatar."""
        #Ralph will have collision solids, so the
        # visible geometry shouldn't collide at all.
        self.prime.setCollideMask(P.BitMask32.allOff())
        # to collide with the terrain & keep ralph grounded
        fromCol(self.prime,Floor,P.CollisionRay(*ray),AllM|GroundM)
        #The ground and walls should have separate collision masks otherwise
        # Pusher can interfere with Floor if the ground gets too steep.
        #The sphere is set to collide with the other things in
        # the world which have mask PusherM
        fromCol(self.prime,Pusher,P.CollisionSphere(*sphere),PusherM)
    def update(self,dt):
        """Call this method in your main loop task."""
        self.prime.setFluidPos(#fluidly update position based on velocity
            self.prime.getX()+self.vel.getX()*dt,
            self.prime.getY()+self.vel.getY()*dt,
            self.prime.getZ() )#let Floor worry about Z
        #recalc velocity to point to destination
        self.vel = self.point-self.prime.getPos()
        if self.vel.lengthSquared()<.1:#Then consider yourself arrived.
            self.request('Stand')#change FSM state
            self.vel=P.Vec3.zero()#stop moving.
        else:
            self.vel.normalize()#set magnitude to 1
            self.vel*=self.speed#the magnitude of velocity is speed.
    def setDestination(self,point):
        """Go to the point."""
        self.point = point
        pr = self.prime.getP(),self.prime.getR()#to preserve pitch and roll
        self.prime.lookAt(self.point)#lookAt affects all three (HPR)
        #keep heading but revert pitch and roll.
        self.prime.setHpr(self.prime.getH(),*pr)
        #subtracting points yeilds a vector pointing from the 1st to the 2nd
        self.vel = self.point-self.prime.getPos()#destination from position
        self.vel.normalize()#set magnitude to 1
        self.vel*=self.speed#the magnitude of velocity is speed.
        self.request('Run')#change FSM state
## State handlers. Only define them if something happens at that transition.
    def enterRun(self):
        self.myActor.loop("run")#loop the run animation
    def exitRun(self):
        self.myActor.stop("run")#stop the run animation
    def enterStand(self):
        self.myActor.pose("walk",6)#both feet on the floor
    #notice that no def exitStand is required.
#end Avatar
class Environ:
    """Setup Environ.

    Loads the model and sets collision masks."""
    def __init__(self):
        self.prime = loader.loadModel("models/world")
        self.prime.reparentTo(render)
        #break the model into the named pieces
        for word in ('terrain','tree','wall','rock','hedge'):
            setattr(self,word,self.prime.find('**/%s*'%word))#isn't python fun?
        #set the collideMasks of the pieces individually
        self.terrain.setCollideMask(AllM|GroundM|CameraM)#Ground and walls \
        self.wall.setCollideMask(AllM|PusherM|CameraM)#should never be between\
        self.rock.setCollideMask(AllM|PusherM|CameraM)#camera and avatar.
        #But it doesn't matter if a tree is in the way. Let the player worry\
        self.tree.setCollideMask(AllM|PusherM)#about that. Besides, geom test\ 
        self.hedge.setCollideMask(AllM|PusherM)#for doodads is too expensive.
#end Environ
class Marker:
    """Setup marker to mark the clicked destination.

    At the moment simply loads jack and turns off his collisions, but you
    could easily make it do so much more."""
    def __init__(self):
        #you probably want to use a different marker model
        self.prime = loader.loadModel('jack')
        self.prime.reparentTo(render)
        self.prime.setScale(.1,.1,.1)
        #this is just a display element, so it shouldn't affect the world.
        self.prime.setCollideMask(P.BitMask32.allOff())#no collisions!
#end Marker
class EdgeScreenTracker(DirectObject):
    """Mouse camera control interface."""
    def __init__(self,avatar,offset=P.Point3.zero(), dist=10,
          rot=20,zoom=(2,20),pitch=(-80,-10)):
        # Disable default camera interface.
        base.disableMouse()
        # Set parameters
        self.zoomLvl = dist #camera starting distance
        self.speed = 1.0/rot # Controls speed of camera rotation.
        self.zoomClamp=zoom#clamp zoom in this range
        self.clampP=pitch#clamp pitch in this range
        self.target = avatar.attachNewNode('camera target')
        self.target.setPos(offset)#offset target from avatar.
        #Load the camera
        self.__loadCamera()
        #Enable new camera interface
        self.accept('zoom in', self.cameraZoom,[0.7])#Translated. For zooming.
        self.accept('zoom out', self.cameraZoom,[1.3])
        taskMgr.add(self.mousecamTask, "mousecamTask")#For edge screen tracking
    def __loadCamera(self):
        """Only seperate for organisation, treat it as is part of __init__() .
        
        Load the camera & setup segmet & queue for detecting obstructions."""
        #Don't rotate the target with the avatar.
        self.target.node().setEffect(P.CompassEffect.make(render))
        camera.reparentTo(self.target)# Attach the camera to target.
        camera.setPos(0,-self.zoomLvl,0)# Position the camera
        self.rotateCam(P.Point2(0,0))# Initialize gimbal clamps.
        self.Q = P.CollisionHandlerQueue()# New queue for camera.
        self.segment = fromCol(self.target,self.Q,
            P.CollisionSegment(P.Point3.zero(),camera.getPos(self.target)),
            P.BitMask32(CameraM))#CameraM into segment between camera & target.
    def mousecamTask(self,task):
        """Rotate camera when the pointer moves to the edges of the screen.
        
        Also temporarily zooms in past an obstructing CameraM'ed object."""
        self.setDist(self.zoomLvl)#preset dist to current zoom level.
        if self.Q.getNumEntries() > 0:#if there was a collision
            self.Q.sortEntries() #so we get the closest collision to avatar
            point=self.Q.getEntry(0).getSurfacePoint(self.target)#get the point
            if point.lengthSquared()<camera.getPos().lengthSquared():#not out.
                self.setDist(point.length())#Temporarily zoom to point.
        camera.lookAt(self.target)# always point camera at target
        if not base.mouseWatcherNode.hasMouse():#See if the mouse is available.
            return Task.cont#if no, just loop again.
        # Get the relative mouse position, its always between 1 and -1
        mpos = base.mouseWatcherNode.getMouse()
        if mpos.getX() > 0.99:
            self.rotateCam(P.Point2(-10,0))
        elif mpos.getX() < -0.99:
            self.rotateCam(P.Point2(10,0))
        if mpos.getY() > 0.9:
                self.rotateCam(P.Point2(0,-3))
        elif mpos.getY() < -0.9:
            self.rotateCam(P.Point2(0,3))
        return Task.cont#loop again.
    def rotateCam(self,arc):
        """Setup a lerp interval to rotate the camera about the target."""
        newP=clampScalar(self.target.getP()-arc.getY(),*self.clampP)#Clamped.
        newH=self.target.getH()+arc.getX()#Not clamped, just added.
        LERP.LerpHprInterval(self.target, self.speed,#Setup the interval\
            P.Vec3(newH,newP,self.target.getR(), ), ).start()#and start it.
    def cameraZoom(self,zoomFactor,):
        """Scale and clamp zoom level, then set distance by it."""
        self.zoomLvl=clampScalar(self.zoomLvl*zoomFactor,*self.zoomClamp)
        self.setDist(self.zoomLvl)
    def setDist(self,newDist):
        """Set camera distance from the target."""
        vec = camera.getPos()
        vec.normalize()#set length to 1
        vec*=newDist#set length to clamped value
        camera.setFluidPos(vec)#move the camera to new distance
        #Move the segment end but keep it a little behind and below the camera.
        self.segment.node().getSolid(0).setPointB(
            self.target.getRelativePoint(camera, P.Point3(0,-2,-1)))
#end EdgeScreenTracker
class World(DirectObject):
    def __init__(self):
        self.avatar = Avatar()#setup ralph
        self.environ = Environ().prime#load environ
        self.marker = Marker().prime#load marker
        EdgeScreenTracker(self.avatar.prime,P.Point3(0,0,1))#setup camera
        self.__setupPicking()
        self.last = 0#for calculating dt in gameLoop
        taskMgr.add(self.gameLoop, "gameLoop")#start the gameLoop task
    def __setupPicking(self):
        """Only seperate for organisation, treat it as is part of __init__() .
        
        Setup ray and queue for picking."""
        self.pickerQ = P.CollisionHandlerQueue()#the handler
        self.picker=fromCol(camera,self.pickerQ,P.CollisionRay(),GroundM)
        self.accept('click',self.OnClick)#translated
        ####uncomment this line to show the collisions:
        ##base.cTrav.showCollisions(render)
    def OnClick(self):
        """Handle the click event."""
        mpos=base.mouseWatcherNode.getMouse()#mouse's screen coordinates
        #This makes the ray's origin the camera and makes 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):
        #find the position of the nearest intersection in renderspace
        if self.pickerQ.getNumEntries() > 0:
            self.pickerQ.sortEntries() #this is so we get the closest object
            self.point=self.pickerQ.getEntry(0).getSurfacePoint(render)
            self.marker.setPos(self.point)#marker indicates destination
            self.avatar.setDestination(self.point)#set the destination
    def gameLoop(self,task):
        """The main game loop."""
        dt = task.time - self.last#time elapsed since the last frame.
        self.last = task.time#for calculating dt the next time
        self.avatar.update(dt)#allow avatar to update itself
        return direct.task.Task.cont#task must return cont to continue a loop
#end World
World()#no need to store to a variable
run()

I don’t know what clamp is. drwr doesn’t even know. But I thought you should see the comment on clampScalar in PythonUtil.

:laughing:

I re-installed my copy of Dungeon Siege to see how the camera behaves. It’s almost the same as the last script except the pitch and zoom clamps are tighter. And some extra behaviors:* When something unimportant obstructs the view of the avatar either nothing happens (like with mosters), or

  • It fades away (like with trees).
    I don’t know how to make a model fade in panda, but doing nothing is easy. I set up some better collide masks to prevent the camera from reacting to trees.
  • If the camera would go through the ground or a cliff or something it zooms in on the avatar to prevent it, or
  • It pitches to an overhead view.
    I never liked the auto pitch (so I’d perfer to leave that to player control), but the zoom in behavior is common to a lot of games. (Like WoW or Zelda) I think I implemented the auto zoom pretty well, but it could be made better with more collision segments.

Anyway, if you guys think this version is okay, then I think it’s time to post it in the Code Snipplets forum.

OMG! Cyan you da man! This is brilliant! I love it. I can’t thankyou enough for taking the time to do this.

I just wish that I could delete my own thread in the Code Snippets forum. This is your work and I feel it should be posted under your name, because you deserve all the credit for it, not me.

I’ve added a little footnote to my thread in the Code Snippets forum, so if you’d like to add your code to it I’d be very flattered :smiley:.

You’re probably sick to death of working on this, but if you wanted to, there are a couple of tiny improvements I’d like to suggest.

  1. When Ralph collides with an obstacle such as a rock, he stops (this is good! :smiley:) but he continues to play his run animation, so it looks like he’s running in place. If possible, I think it would be better if he just stopped and stood still after he collided with something.

  2. I really love how you’ve stopped the camera from going through objects, zooming in on the player instead is much better. However, it just doesn’t look right when it does this with the terrain. To be honest, I think I prefer the ‘Roaming Ralph’ method, where the camera stays 2 feet above the ground, so as Ralph goes up and down the hills, the camera also moves up and down (so it never actually touches the terrain).

These are two very minor things though. The rest of it is just marvelous. Thankyou so much for all your help with this.

Cheers

Hmm . . . Possibilities. You’ll have to help me with these :slight_smile:

I’ve been using a [color=darkred]CollisionHandlerPusher with a collision sphere around Ralph to keep him out of things. It should sort of slide Ralph around anything he runs into, but its performance is mediocre on visible geometry; Ralph can easily get stuck if there isn’t room between objects, and there are some unsightly camera jitters if he runs into anything pointy… :stuck_out_tongue:

Most of this could be easily fixed, of course, if there was a nice and smooth world collision geometry separate from the visible world geometry. But since there isn’t one (to my knowledge), and you want everyone to be able to use the model included with Panda, it would have to be created programmatically. Well within possible, but maybe more work than it’s worth, considering that any 3d modeling package could make a separate collision geometry.

The only other options are to not use the [color=darkred]CollisionHandlerPusher.
First option: add a pathfinding algorithm, to walk Ralph around the obstacles to the destination. (This might be worth looking into for a Dungeon Siege-like game, but it takes work. I’d rather post it as a separate module.)

Or, second option: just stop Ralph and leave the pathfinding to the human, as you suggested. This would be easy enough to code:

Simply replace the [color=darkred]CollisionHandlerPusher with a [color=darkred]CollisionHandlerEvent. Then translate the appropriate event (When Ralph’s sphere hits a PusherM object) through the Controls class to fire a ‘StopAvatar’ event, which would be handled by a new OnStopAvatar method in the World class that simply sets the destination to the avatar’s previous position. Easy. :wink:

You’ll notice that I added edge screen tracking to the top and bottom edges of the screen to control the pitch, just like Dungeon Siege. I thought this form of camera control would work best just with the auto zoom. As long as the camera is far enough overhead the terrain doesn’t really affect the zoom. (It’s easy to pass a tighter pitch clamp if you prefer to restrict that.)

Try out using the other edges (assuming you haven’t already). If you still don’t like it controlled that way, it is possible to use the [color=darkred]CollisionHandlerFloor on the camera as well. However, the pitch controls would have to be turned off, because they would interfere with each other. If you still wanted edge screen tracking on the top and bottom edges you would have to make them change the height the camera hovers above the ground instead of the camera pitch.

So. . . what do you want?

Cyan you’re the best! I thought for sure you’d be sick of this by now.

Ok, first things first. I think your idea of having ‘world collision geometry’ seperate from the visible world geometry is the best idea. It will mean using a different terrain model, but I think I can handle this (I’m better at arty stuff than I am at programming :smiley:).

So I’ll try my hand at creating a simple level in 3DS Max, if it works out, I’ll upload it to a file host so you and anybody else can download it (perhaps there’s some way that I could even add it to the Panda3D models library).

Anyway, second thing. A pathfinding algorithm. I would kill for this! I’ve looked at *A Star, but I understood maybe 1 word in 10 (it’s WAY beyond me). So if you can write a pathfinding module for Panda3D, I think it will be one of the best contributions anybody has ever made for this community.

But for now, I think I’ll follow your advice and just experiment with the CollisionHandlerEvent. Easy! :open_mouth: Ha! For you maybe, but not for me :unamused:. Still, I’ll have a go at it (just expect lot’s of silly questions :wink:).

As for ‘edge screen tracking’ I’m quite happy not to have edge screen tracking on the top and bottom edges of the screen, you see, I forgot about this, but I disabled the camera from pitching up and down in Dungeon Siege, cause it was making me seasick :unamused:. So all I really want is for the camera to rotate left and right when the mouse pointer moves to either side of the screen.

Well, I’ll leave you in peace for now and go play with 3DS Max for a bit. Thanks heaps.