Point & Click Turning Bug!

Hi All, I hope you’ll forgive me for starting a new thread. I was going to add this to my original thread, but that one’s getting a bit long and I thought that this new post might get overlooked.

Anyway, I’ve made some progress on my point & click player controls. I think I’ve solved the problem of the model moving backwards to a clicked position. It seems that to work correctly, the model’s orientation must be set in your modeling package to face away from the camera (when I used a model that had been modeled and exported facing backwards, it worked a treat :smiley:).

But now I have a new problem. My code to make the player turn towards a clicked position, seems to have a bug in it.

This is what I want to happen. I click a position on the ground (the red X represents the clicked position).

The player should make a half turn to the right and face in the direction of the clicked position:

But this is what actually happens. I click this position on the ground and the player makes a full turn in the opposite direction and then faces in the right direction.

If I click in the opposite corner the same thing happens:

The strange thing is, this only happens when I click in the bottom corners of the map. When I click in the top corners of the map it works just fine.

Anyhow, this is the code:

# This program turns a model to the position (point 3) of a left mouse click 
# on a 3d surface.

import direct.directbase.DirectStart # Start Panda 
from pandac.PandaModules import * # Import the Panda Modules
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
import math # To use math (sin, cos..etc)
from math import sqrt
import sys  

class Picker(DirectObject): 
    def __init__(self): 
        base.disableMouse()
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # Declare variables
        self.position = None
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)
        self.player = loader.loadModel("MODELS/fleet")
        self.player.reparentTo(render)
        self.player.setPos(0, 0, 0)
        self.player.setHpr(0, 0, 0)
        self.player.setColor(Vec4(0, 148, 213, 1))
        # Setup collision stuff. 
        self.picker= CollisionTraverser() 
        self.queue=CollisionHandlerQueue() 
        self.pickerNode = CollisionNode('mouseRay') 
        self.pickerNP = camera.attachNewNode(self.pickerNode) 
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 
        self.pickerRay = CollisionRay() 
        self.pickerNode.addSolid(self.pickerRay) 
        self.picker.addCollider(self.pickerNode, self.queue)
        # Setup controls
        self.accept("escape", sys.exit) 
        self.accept('mouse1', self.moveToPosition)

    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

    def moveToPosition(self):
        # Get the clicked position 
        self.getPosition(base.mouseWatcherNode.getMouse())
        # This is the "start" position
        sPos = self.player.getPos()
        # Calculate the new hpr
        # Create a tmp NodePath and set it to the start position
        # Then make it "lookAt" the position we want to be at.
        # It will then create the hpr that we can turn to.
        a = NodePath("tmp")
        a.setPos(sPos)
        a.lookAt(self.position) # Look at the clicked position.
        newHpr = a.getHpr()
        # Create a turn animation from current hpr to the calculated new hpr.
        playerTurn = self.player.hprInterval(.2,Point3(newHpr[0],newHpr[1],newHpr[2]))
        playerTurn.start()

p = Picker() 

run()

I’m really baffled by this one, so if anybody has any suggestions please advise me.

Cheers

You can import a function from direct.showbase.PythonUtil called closestDestAngle. It should help.

def closestDestAngle(src, dest):
    # The function above didn't seem to do what I wanted. So I hacked
    # this one together. I can't really say I understand it. It's more
    # from impirical observation... GRW
    diff = src - dest
    if diff > 180:
        # if the difference is greater that 180 it's shorter to go the other way
        return src - (diff - 360)
    elif diff < -180:
        # or perhaps the OTHER other way...
        return src - (360 + diff)
    else:
        # otherwise just go to the original destination
        return dest

Also, creating a temp nodepath every move might cause some extra memory usage. Its possible Panda will keep the nodepath around in the scene graph even though your function has lost its scope. It would be better to create a single nodepath and parent it to your player. Then you can call lookAt() on that single nodepath each move instead of creating a new one each time.

Thanks Russ. I’ve tried to implement your suggestion, but I just don’t have the knowledge or experience to make it work properly. This is what I’ve done (don’t laugh, I really tried my best :unamused:):

    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
        
    def moveToPosition(self):
        # Get the clicked position 
        self.getPosition(base.mouseWatcherNode.getMouse())
        # This is the "start" position
        sPos = self.player.getPos()
        diff = sPos - self.position # Start position - the clicked position.
        if diff > 180:
            # if the difference is greater that 180 it's shorter to go the other way
            return sPos - (diff - 360)
        elif diff < -180:
            # or perhaps the OTHER other way...
            return sPos - (360 + diff)
        else:
            # otherwise just go to the original destination
            return self.position
        
        playerTurn = self.player.hprInterval(.5,self.position)
        playerTurn.start()

p = Picker() 

run() 

Sadly, this code doesn’t work. But it looks very similar to the code I’m using to MOVE the player to the clicked position (this particular piece of code works nicely):

    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() 
            self.position = self.queue.getEntry(0).getSurfacePoint(self.environ) 
            return None

    def moveToPosition(self):
        # Get the clicked position 
        self.getPosition(base.mouseWatcherNode.getMouse())
        # This is the "start" position
        sPos = self.player.getPos()
        # 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
        if sPos[0] > self.position[0]:
            distanceX = sPos[0] - self.position[0]
        else:
            distanceX = self.position[0] - sPos[0]
            
        if sPos[1] > self.position[1]:
            distanceY = sPos[1] - self.position[1]
        else:
            distanceY = self.position[1] - sPos[1]
            
        distance = sqrt((distanceX * distanceX) + (distanceY * distanceY))
 
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position, startPos = sPos) 
        playerMove.start()

If I can get this code to work without the need for a dummy node, that’d be great, because I already have a camera_dummy_node attached to the player (so that I can rotate the camera around him).

Sorry to be such a pest. Can you see what I’ve done wrong?

Thanks very much.

Oops, I forgot to mention, I eventually plan to combine both playerTurn and playerMove into the one function and then play them in sequence. Hopefully this will make the player turn in the direction of the mouse click and then move to that position.

Something like the following code (this is Sandman’s example :smiley:):

  def moveToPosition(self):
    if self.playerMovement != None:
        self.playerMovement.pause()
        self.__stopWalkAnim()
       
    # This is the "start" position
    sPos = self.player.getPos()

    # Calculate the new hpr
    # Create a tmp NodePath and set it to the start position
    # Then make it "lookAt" the position we want to be at.
    # It will then create the hpr that we can turn to.
    a = NodePath('tmp')
    a.setPos(sPos)
    a.lookAt(position)
    newHpr = a.getHpr()

    # Create a turn animation from current hpr to the calculated new hpr.
    playerTurn = self.player.hprInterval(1,Point3(newHpr[0] + self.rotationOffset,newHpr[1],newHpr[2]),startHpr = self.player.getHpr())

    # 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 coordinate base on self.movementSpeed
    if sPos[0] > position[0]:
      distanceX = sPos[0] - position[0]
    else:
      distanceX = position[0] - sPos[0]
   
    if sPos[1] > position[1]:
      distanceY = sPos[1] - position[1]
    else:
      distanceY = position[1] - sPos[1]

    distance = sqrt((distanceX * distanceX) + (distanceY * distanceY))
   
    # Create a movement animation using the 2 positions
    playerMove = self.player.posInterval( (distance / self.movementSpeed), position, startPos = sPos)

    # Put the animations into a sequence and create an event that will be
    # called when its finished.
    self.playerMovement = Sequence(playerTurn, playerMove, name = 'playerMove')
    self.playerMovement.setDoneEvent('player-stopped')

    # Start the walking animation, then start the turn+move anims
    self.player.loop('walk')
    self.playerMovement.start()

  def __stopWalkAnim(self):
    # This is called when the turn+move anim has finished.
    # We can then stop the walk anim.
    self.player.stop('walk')
    self.playerMovement = None 

Cheers

So here is more what I was talking about. The periods represent the code already in there. Hope its not too confusing.

#imports
.
.
.
from direct.showbase.PythonUtil import closestDestAngle
.
.
.
class Picker(DirectObject):
    def __init__(self):
        .
        .
        .
        #after self.player has been set up
        self.npLook = self.player.attachNewNode("npLook")
        .
        .
        .
    def moveToPosition(self):
        # Get the clicked position
        self.getPosition(base.mouseWatcherNode.getMouse())
        
        # Calculate the new hpr
        self.npLook.lookAt(self.position) # Look at the clicked position.
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr(render)
        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()
       
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position, startPos = sPos)

        self.playerMovement = Sequence(playerTurn, playerMove)
        self.playerMovement.setDoneEvent('player-stopped')
        # Start the walking animation, then start the turn+move anims
        self.player.loop('walk') 
        self.playerMovement.start()
        .
        .
        .

Thankyou so much for this Russ. This time I was able to get the code working. But sadly, it’s still not doing what I want it to do :imp:.

The player now makes a small quarter turn in the direction of the click, which results in him sometimes moving sideways or even backwards to the clicked position (sometimes he turns and moves correctly too, it just depends on where you click :unamused:).

It’s kind of difficult to explain. So it’s probably better to show you. I don’t know if this is of any use, or even if you’d have the time to test it, but I’ve uploaded the models that I’m using to Rapidshare. So if you want to, you could take a look at it with the same models:

rapidshare.de/files/18379082/Tes … s.rar.html

And this is the new code (Note: The Fleet model isn’t animated, so I removed the animation references):

# This program turns a model to the position (point 3) of a left mouse click 
# on a 3d surface and then moves it to that position.

import direct.directbase.DirectStart # Start Panda 
from pandac.PandaModules import * # Import the Panda Modules
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
import math # To use math (sin, cos..etc)
from math import sqrt
from direct.showbase.PythonUtil import closestDestAngle
import sys  

class Picker(DirectObject): 
    def __init__(self): 
        base.disableMouse()
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # Declare variables
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 8.0 # Controls how fast the player moves.
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)
        self.player = loader.loadModel("MODELS/fleet")
        self.player.reparentTo(render)
        self.player.setPos(0, 0, 0)
        self.player.setHpr(0, 0, 0)
        self.player.setColor(Vec4(0, 148, 213, 1))
        self.npLook = self.player.attachNewNode("npLook")
        # Setup collision stuff. 
        self.picker= CollisionTraverser() 
        self.queue=CollisionHandlerQueue() 
        self.pickerNode = CollisionNode('mouseRay') 
        self.pickerNP = camera.attachNewNode(self.pickerNode) 
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 
        self.pickerRay = CollisionRay() 
        self.pickerNode.addSolid(self.pickerRay) 
        self.picker.addCollider(self.pickerNode, self.queue)
        # Setup controls
        self.accept("escape", sys.exit) 
        self.accept('mouse1', self.moveToPosition)

    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
        
    def moveToPosition(self):
        # Get the clicked position 
        self.getPosition(base.mouseWatcherNode.getMouse())
        # Calculate the new hpr
        self.npLook.lookAt(self.position) # Look at the clicked position.
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr(render)
        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()
       
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position)

        self.playerMovement = Sequence(playerTurn, playerMove)
        self.playerMovement.start() 
        
p = Picker() 

run()

This has got to be one of the hardest and most frustrating things I’ve ever tried to do. I can’t tell you how much I appreciate your help with this. My respect and awe for game programmer’s has increased enormously.

Thanks heaps.

I didn’t really test the code before I posted.

change newH to:


newH = currHpr[0] + closestDestAngle(currHpr[0], newHpr[0]) 

I think that should fix it, but I haven’t tested this either.
If it still acts funny, use some print statements to help figure out what is going on.

nevermind that last post, it won’t work either. Let me actually think about it and I will post something new

so this should work:

.
.
.
        #in __init__
        self.npLook = render.attachNewNode("npLook")
    .
    .
    .
        #in moveToPosition
        # Calculate the new hpr 
        self.npLook.setPos(self.player.getPos())
        self.npLook.lookAt(self.position) # Look at the clicked position.
        currHpr = self.player.getHpr()
        newHpr = self.npLook.getHpr()
        newH = closestDestAngle(currHpr[0], newHpr[0]) 
    .
    .
    .

Oh Wow!!! This is fantastic!!!

Russ, I think you’ve done it. I’ve just tested it, and it works almost perfectly. It’s a HUGE improvement over the original code.

But there’s just one little hiccup, that weird ‘spin around in the opposite direction’ bug is still there, but not as bad as before. This time it only occurs in 3 places.

The first time is when traveling in a straight line ‘north to south’ through the middle of the map, if I first click on a point in the middle of the map, the player moves nicely to that point. But if I click again to resume traveling (in the same direction), the player spins a full circle before he moves to the next click.

The next two occurences are when traveling clockwise and anti-clockwise around the edges of the map (by clicking in each corner). I’ve made a quick image to show you what I mean (this nasty little bug is consistent and repeatable, it happens in the same spots over and over).

Anyway, bugs or no bugs, I can’t thankyou enough for taking the time to work on this for me. This thing has had me almost in tears. I didn’t think I’d ever solve it. Without you’re help, I think I’d have given up on it. Thankyou a thousand times over.

Cheers

Those extra spins have to do with wrapping of the heading angle. Unfortunately, closestDestAngle doesn’t account for that. One way to fix it would be to correct for the wrap yourself some time after the player hits its final heading (after the turn interval or after the enitre movement sequence). You could do this with a call like:

reducedH = self.player.getH()%360.0
self.player.setH(reducedH)

Thanks Russ. I’ve just tried it. I tried putting it after the turn interval and then after the movement sequence, but in both cases, all it seemed to do was make the player spin in each one of the corners, instead of just the one :unamused:.

I’ve also discovered that swearing and threatening it with dire consequences doesn’t work either :smiley:. Do you have any ideas for what I should try next?

Thanks heaps

Given how many other things Python and Panda3d handle automatically, I almost wouldn’t be surprised if there were a “swearing and threatening dire consequences” detector. Obviously, someone will have to build one in :wink:

try:
  myFunction(defaultOptions)
except SwearingAndThreateningDireConsequences:
  myFunction(-defaultOptions)

If you find that it’s spinning the wrong direction in all cases now, that’s good news: it probably means something in the equation is negative that should be positive. Maybe change an addition to a subtraction and try again?

It might also be helpful to walk through the calculation on paper. Write down the starting heading and your desired heading, then see if you can guess how the lerp function will go from one to the other. Doing this can help you isolate what about the lerp’s behavior isn’t matching up with your expectations. Even if it doesn’t solve the problem directly, you may gain a deeper insight to how the lerps work; that will almost always be helpful down the road.

Best of luck!
-Mark

[/code]

Bummer! I’ve tried everything I can think of, I even tried defining ‘closestDestAngle’ as a function in my own code so that I could play with its values, but all to no effect.

I’m at my wit’s end here, it just seems to be an impossible task to get this working properly. I’m beginning to suspect that Panda3D might simply not be capable of implementing point & click game controls :cry: (sadly, a lot of engines can’t).

I’ve tried searching the internet for some type of point & click example codes (even ones written in another language, just to give me some idea of what to do) but I couldn’t find anything of use. Grrr I just don’t know what else to do.

Anyway, here is my ‘almost’ working code:

# This program turns a model to the position (point 3) of a left mouse click 
# on a 3d surface.

import direct.directbase.DirectStart # Start Panda 
from pandac.PandaModules import * # Import the Panda Modules
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
import math # To use math (sin, cos..etc)
from math import sqrt
import sys  

class Picker(DirectObject): 
    def __init__(self): 
        base.disableMouse()
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # Declare variables
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 8.0 # Controls how fast the player moves.
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)
        self.player = loader.loadModel("MODELS/fleet")
        self.player.reparentTo(render)
        self.player.setPos(0, 0, 0)
        self.player.setHpr(0, 0, 0)
        self.player.setColor(Vec4(0, 148, 213, 1))
        self.npLook = render.attachNewNode("npLook") 
        # Setup collision stuff. 
        self.picker= CollisionTraverser() 
        self.queue=CollisionHandlerQueue() 
        self.pickerNode = CollisionNode('mouseRay') 
        self.pickerNP = camera.attachNewNode(self.pickerNode) 
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 
        self.pickerRay = CollisionRay() 
        self.pickerNode.addSolid(self.pickerRay) 
        self.picker.addCollider(self.pickerNode, self.queue)
        # Setup controls
        self.accept("escape", sys.exit) 
        self.accept('mouse1', self.moveToPosition)
        
    def closestDestAngle(src, dest):
        diff = src - dest
        if diff > 180:
            # if the difference is greater that 180 it's shorter to go the other way
            return src - (diff - 360)
        elif diff < -180:
            # or perhaps the OTHER other way...
            return src - (360 + diff)
        else:
            # otherwise just go to the original destination
            return dest

    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
        
    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.
        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(newHpr[0], 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()
        # Put the animations into a sequence.
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position)
        self.playerMovement = Sequence(playerTurn, playerMove)
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH)
        self.playerMovement.start()
        
p = Picker() 

run()

Cheers

The way it is now, the heading is being reduced before the movement sequence starts. This is why the player is spinning each time. The reduction needs to happen after the turn interval has finished.

    #define this somewhere
    def reducePlayerH(self):
        """reduce the player heading to eliminate wrap-around"""
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH) 
        .
        .
        .
        
        #in moveToPosition
        self.playerMovement = Sequence(playerTurn, Func(self.reduceH), playerMove) 
        

Alternatively, you can reduce the player heading before you calculate the closesDestAngle:


        # 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]) 

Thankyou for your infinite patience Russ, I am so incredibly grateful for all your help.

Anyway, I tried your second solution first (cause it looked easier :wink:) but sadly, it didn’t seem to make any difference to the spin bug.

So onto the next one, I defined a reducePlayerH() function and ran the code, but I’m getting a strange error message telling me that global name ‘reducedH’ is not defined:

I’ve stuffed something up again haven’t I? This is how I updated the code (I tried declaring a variable ‘self.reducedH = None’ but that doesn’t seem to help):

# This program turns a model to the position (point 3) of a left mouse click 
# on a 3d surface.

import direct.directbase.DirectStart # Start Panda 
from pandac.PandaModules import * # Import the Panda Modules
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
import math # To use math (sin, cos..etc)
from math import sqrt
import sys  

class Picker(DirectObject): 
    def __init__(self): 
        base.disableMouse()
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # Declare variables
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 8.0 # Controls how fast the player moves.
        self.reducedH = None
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)
        self.player = loader.loadModel("MODELS/fleet")
        self.player.reparentTo(render)
        self.player.setPos(0, 0, 0)
        self.player.setHpr(0, 0, 0)
        self.player.setColor(Vec4(0, 148, 213, 1))
        self.npLook = render.attachNewNode("npLook")
        # Declare functions.
        self.reducePlayerH()
        # Setup collision stuff. 
        self.picker= CollisionTraverser() 
        self.queue=CollisionHandlerQueue() 
        self.pickerNode = CollisionNode('mouseRay') 
        self.pickerNP = camera.attachNewNode(self.pickerNode) 
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 
        self.pickerRay = CollisionRay() 
        self.pickerNode.addSolid(self.pickerRay) 
        self.picker.addCollider(self.pickerNode, self.queue)
        # Setup controls
        self.accept("escape", sys.exit) 
        self.accept('mouse1', self.moveToPosition)
        
    def closestDestAngle(src, dest):
        diff = src - dest
        if diff > 180:
            # if the difference is greater that 180 it's shorter to go the other way
            return src - (diff - 360)
        elif diff < -180:
            # or perhaps the OTHER other way...
            return src - (360 + diff)
        else:
            # otherwise just go to the original destination
            return dest

    def reducePlayerH(self):
        # Reduce the players heading to eliminate wrap-around
        self.reducedH = self.player.getH()%360.0
        self.player.setH(reducedH) 

    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
        
    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.
        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(newHpr[0], 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()
        # Put the animations into a sequence.
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position)
        self.playerMovement = Sequence(playerTurn, Func(self.reducedH), playerMove)
        self.playerMovement.start()
        
p = Picker() 

run()

What have I done wrong?

Thanks heaps

you define self.reducedH but reference reducedH which python thinks is a global variable

change reducedH to a local variable in the function so instead of

self.reducedH = player.getH()%360.0

use

reducedH = player.getH()%360

Thanks Russ, I did as you suggested, but now I’m getting a new error message (:evil: it’s enough to make you scream, isn’t it?).

And the new code:

# This program turns a model to the position (point 3) of a left mouse click 
# on a 3d surface.

import direct.directbase.DirectStart # Start Panda 
from pandac.PandaModules import * # Import the Panda Modules
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
import math # To use math (sin, cos..etc)
from math import sqrt
import sys  

class Picker(DirectObject): 
    def __init__(self): 
        base.disableMouse()
        # Position the camera
        camera.setPos(0, -35, 18) # X = left & right, Y = zoom, Z = Up & down.
        camera.setHpr(0, -25, 0) # Heading, pitch, roll.
        # Declare variables
        self.position = None
        self.playerMovement = None
        self.movementSpeed = 8.0 # Controls how fast the player moves.
        self.reducedH = None
        # Load an environment
        self.environ = loader.loadModel("MODELS/grass")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)
        self.player = loader.loadModel("MODELS/fleet")
        self.player.reparentTo(render)
        self.player.setPos(0, 0, 0)
        self.player.setHpr(0, 0, 0)
        self.player.setColor(Vec4(0, 148, 213, 1))
        self.npLook = render.attachNewNode("npLook")
        # Declare functions.
        self.reducePlayerH()
        # Setup collision stuff. 
        self.picker= CollisionTraverser() 
        self.queue=CollisionHandlerQueue() 
        self.pickerNode = CollisionNode('mouseRay') 
        self.pickerNP = camera.attachNewNode(self.pickerNode) 
        self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) 
        self.pickerRay = CollisionRay() 
        self.pickerNode.addSolid(self.pickerRay) 
        self.picker.addCollider(self.pickerNode, self.queue)
        # Setup controls
        self.accept("escape", sys.exit) 
        self.accept('mouse1', self.moveToPosition)
        
    def closestDestAngle(src, dest):
        diff = src - dest
        if diff > 180:
            # if the difference is greater that 180 it's shorter to go the other way
            return src - (diff - 360)
        elif diff < -180:
            # or perhaps the OTHER other way...
            return src - (360 + diff)
        else:
            # otherwise just go to the original destination
            return dest

    def reducePlayerH(self):
        # Reduce the players heading to eliminate wrap-around
        reducedH = self.player.getH()%360.0
        self.player.setH(reducedH) 

    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
        
    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.
        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(newHpr[0], 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()
        # Put the animations into a sequence.
        playerMove = self.player.posInterval((distance / self.movementSpeed), self.position)
        self.playerMovement = Sequence(playerTurn, Func(self.reducedH), playerMove)
        self.playerMovement.start()
        
p = Picker() 

run()

I dunno, maybe I’m just not cut out for this programming business :cry:.

Thanks again

As russ told you above: self.reduceH is a virable
Func( … ) needs a callable object
I guess you should use Func( self.reducePlayerH ) instead
Just for the future:

self.playerMovement = Sequence(playerTurn, Func(self.reducedH), playerMove)
File "C:\Panda3D-1.0.5\direct\src\interval\FunctionInterval.py", line 275, in
__init__
assert(callable(function)) 

assert is a functions that checks a logical expression.
You see there is something that needs to be a function but is not a function
Then look at the line bove. There you can see the Func( … ) and then try to find whats the mistake
Hope this helps
Martin

Thanks Martin, that fixed the AssertionError. But after all that trouble, that hateful spin bug is still there. The new function doesn’t seem to have made any difference at all :frowning:.

Again HUGE thanks to all of you.