How can I stop my camera turning with the player?

Hi all,

Sorry to do this to you, but I need your help again :unamused:. My code for point & click game controls is almost finished, but I have one slight problem remaining; when my player turns, so too is the camera.

At the moment, to enable the camera to rotate around the player, I have a camera_dummy_node parented to the player and the camera itself parented to the camera_dummy_node. Because of this, the camera seems to be inheriting the Hpr of the player, so when the player turns the camera also turns.

What I want, is for the camera to only rotate when I move the mouse pointer to the edges of the screen (or I use the left & right arrow keys). But although I want the camera to rotate independently of the player, I also want it to remain ā€˜fixedā€™ on him, to follow him as he moves and to keep him centered in view at all times (which it currently does).

Anyway, IĆ¢ā‚¬ā„¢ve made a piccy to show (hopefully) what I mean:

In the top image, imagine that IĆ¢ā‚¬ā„¢ve rotated the camera to look at the player from the side. I then click a location to the right of the player.

The second image illustrates what IĆ¢ā‚¬ā„¢d like to happen, the player should turn to face the direction of the click (and then move to that position), but the camera shouldnĆ¢ā‚¬ā„¢t turn, it should remain looking in the direction that it was rotated to (which would now be behind the player).

The third image illustrates what is currently happening, as the player changes his heading, so too does the camera (so itĆ¢ā‚¬ā„¢s still looking at him from the side) which is very bad (especially if you suffer from motion sickness :wink:).

This is the code:

# 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()
        # 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 collision stuff.
        self.picker= CollisionTraverser() # Make a traverser 
        self.queue=CollisionHandlerQueue() # Make a handler
        #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)
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        self.pickerRay = CollisionRay() # Make our ray
        self.pickerNode.addSolid(self.pickerRay) # Add it to the collision node 
        #Register the ray as something that can cause collisions
        self.picker.addCollider(self.pickerNode, self.queue)
        # 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/grass")
        self.environ.reparentTo(render) # Make it display on the screen.
        self.environ.setPos(0, 0, 0)
        self.environ.setHpr(0, 0, 0)
        # Load the player and its animations
        self.player = Actor.Actor("MODELS/ralph",{"walk":"MODELS/ralph-walk"})
        self.player.reparentTo(render) # Make it display on the screen.
        self.player.setPos(0, 0, 0) # Position it at the center of the world.
        self.player.setHpr(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")
        #Position the camera dummy node.
        self.camera_dummy_node.setPos( 0, 0, 0)
        self.camera_dummy_node.setHpr(0, 0, 0) 
        # Attach the camera dummy node to the player.
        self.camera_dummy_node.reparentTo(self.player)
        # Attach the camera to the 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 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())
        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())
        self.npLook.lookAt(self.position) # Look at the clicked position.
        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(.2, 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.getPos()
        distance = travelVec.length()
        # Create an animation to make the player move to the clicked position
        playerMove = self.player.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()

Does anybody know if there is some way to stop the camera from turning with the player?

Thanks heaps

The long and the short of it is: if the camera is parented to the player, then for the most part, itā€™s going to move/rotate/scale with the player. Probably, you donā€™t want to parent the camera to the player. Instead, you just want to create a task to move the camera around.

The latest version of panda, version 1.2.1, contains a new sample program: ā€œRoaming Ralph.ā€ The main purpose of the sample program is to show just exactly how this sort of thing might be done.

Wow Josh! That was fast :smiley:. Thanks for the quick reply. I didnā€™t know there was a new version of Panda, Iā€™m off to download it right now.

Cheers

Oh no! Iā€™ve just downloaded and installed the new version of Panda, but now my code wonā€™t run at all :frowning: .

Itā€™s giving me this error:

I did do a search for the problem, but what I found didnā€™t make any sense. Iā€™m using the collision code from the manualā€™s ā€˜Example for clicking on 3D objectsā€™.

Somebody really needs to update the manual, but for now, which lines do I need to change?

Cheers

It looks like you may have tripped over one of the dangers of upgrading any library: it seems that perhaps an old way of doing things has been changed. In this case, the method for adding colliders may have changed slightly.

Iā€™m afraid I havenā€™t yet taken the plunge to Panda3d 1.2.1, but based on the error message, Iā€™d recommend trying something like the following and seeing if it works. This is a total guess, so if Iā€™m wrong, Iā€™m sure someone will fill in the details.

Instead of doing


self.picker.addCollider(self.pickerNode, self.queue) 

Try

pickerNodePath=NodePath('pickerNodePath')
pickerNodePath.attachNewNode(self.pickerNode)
pickerNodePath.reparentTo(<whatever was holding self.pickerNode>)
self.picker.addCollider(pickerNodePath, self.queue)

(note of course that if you want to use pickerNodePath later, youā€™ll need to retain it by calling it self.pickerNodePath).

Best of luck!
-Mark

Oh, on the main thread topic:

Usually when I run into a problem like this, I find that itā€™s easiest to fix by adding another layer of abstraction. In this case, your camera is coupled to your person in the world, and youā€™d like to decouple it from the personā€™s facing but not the personā€™s position. I would guess you have your render tree is setup something like this right now:

<render>
|-<guy>
  |-<camera>

To decouple the cameraā€™s rotation from the guyā€™s rotation, try something like this:

<render>
|-<guy_position_node> (created with NodePath("guy_position"))
  |-<camera>
  |-<guy>

Now when you move the guy around in the world, do so by changing the location of <guy_position_node>. But, when you want to change the guyā€™s facing, do so by changing the H value of the node. Turning the node wonā€™t change the cameraā€™s orientation, because the camera is actually looking at <guy_position_node>.

Hope that helps!

Take care,
Mark

Thanks Mark, the collision error is now fixed, and my code will run again ā€˜phew!ā€™.

Now, I tried to implement what you suggested, but Iā€™m very new to programming and Iā€™m afraid that Iā€™m not very good at it. So this is what I did:

    def loadModels(self):
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render) # Make it display on the screen.
        self.environ.setPos(0, 0, 0)
        self.environ.setHpr(0, 0, 0)
        # Load the player and its animations
        self.player = Actor.Actor("MODELS/ralph",{"walk":"MODELS/ralph-walk"})
        self.player.reparentTo(render) # Make it display on the screen.
        self.player.setPos(0, 0, 0) # Position it at the center of the world.
        self.player.setHpr(0, 0, 0)
        # Create a dummy node for the player turn function
        self.npLook = render.attachNewNode("npLook")
        # Create a player dummy node
        self.player_dummy_node = render.attachNewNode("player_dummy_node")
        # Attach the player dummy node to the player.
        self.player_dummy_node.reparentTo(self.player)
        #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

I created a ā€˜player_dummy_nodeā€™ and reparented it to the player, I then reparented the ā€˜camera_dummy_nodeā€™ to the ā€˜player_dummy_nodeā€™ and then reparented the camera to the ā€˜camera_dummy_nodeā€™ (yeah, it confuses me too :unamused:).

This of course doesnā€™t work :confused: but it feels like Iā€™m on the right track, any pointers to where I went wrong?

Cheers

Hey Tiptoe,

Youā€™re definitely on the right track. Based on what youā€™ve written (ignoring the nodes that donā€™t have to do with the actor), you get a render tree that looks like this:

<render>
|-<player>
  |-<player_dummy_node>
    |-<camera_dummy_node>
      |-camera
|-<npLook>

(and I think npLook can be discarded; is it used later?)

You want a render tree that looks like this:

<render>
  |-<player_dummy_node>
    |-<player>
    |-<camera_dummy_node>
      |-camera

In other words: the player model and the camera dummy node are ā€œsiblingsā€ under the player dummy node. So moving the player dummy node moves both the player and the camera, but rotating the player node doesnā€™t rotate the camera.

Try making the following change: instead of

self.player.reparentTo(render)

, do

self.player.reparentTo(self.player_dummy_node)

(lower in the code, after youā€™ve created player_dummy_node).

Then, to move the player around:


self.player_dummy_node.setPos(x,y,z)

And to turn the player:


self.player.setH(newH)

Now your rotations are independent of your translations. Since the camera is (indirectly) a child of player_dummy_node, it will get moved; however, since it is not a child of player, it wonā€™t get rotated.

Best of luck!

-Mark

Fantastic Mark! That works!

Thereā€™s just one little problem, my player has suddenly stopped turning fully to face in the direction of the mouse click. Now heā€™s only making a quarter turn and then he sort of moves sideways to the clicked position.

Hereā€™s the parts of the code that I changed (self.npLook is used for turning the player). In this section I load the models and create the dummy nodes:

This section of code contains all the movement code (Iā€™m heavily indebted to Russ for working this out for me and solving the spinning/wrapping bug that I had):

I think Iā€™ve missed something, any ideas?

Cheers

change


self.npLook.setPos(self.player.getPos()) 

to


self.npLook.setPos(self.player_dummy_node.getPos())

or


self.npLook.setPos(self.player.getPos(render))

the player is no longer moving directly so its position will always return (0,0,0) with respect to its parent dummy node.

The second way will stay consistent if the render tree changes again.

That fixed it! You are my hero Russ :smiley:. Thankyou very, very much.

Oh this code is perfect now, I canā€™t tell you how happy I am with it. Itā€™s taken me weeks and weeks to do it, but this wonderful result has made it all worthwhile.

It now works just like the point & click controls in Dungeon Siege and NeverWinter Nights. Which is exactly what I was hoping to achieve. Just wonderful! :smiley:

In fact, Iā€™m so pleased with this, that I intend to share it with everybody. I just need to adapt it to the new ā€˜Roaming Ralphā€™ example (talk about good timing :wink:) so that the player can also walk on uneven terrain and properly collide with trees and rocks and whatnot. Then Iā€™ll post it in the Code Snippets forum.

Thanks again Mark and Russ.