camera controls interfere with GUI input

I have a camera class where I allow to rotate the camera by holding down the left mouse button and dragging the mouse. I have this line in my task

base.win.movePointer(0, base.win.getXSize()/2, base.win.getYSize()/2)

This makes sure the mouse won’t get to the edge of the window and prevent the player from rotating the camera before moving the mouse away from the edge and pressing+dragging again. But there is a problem now that every time the player clicks on a DirectGUI button, the mouse jumps to the middle of the screen. What can be done to avoid this?

When the user clicks on a DirectGui button, it will not send the “mouse1” event, but it will still send “mouse1-up” when he releases the button. So you can deal with this particular problem by listening for these two events, and only activating the mouse-resetting mode after you have heard “mouse1” and until you have heard “mouse1-up”.

But I think you have a bigger problem with your design. First, note that base.win.movePointer() isn’t portable unless you first put the mouse in “relative” mode. (It doesn’t work as well on Linux, and it doesn’t work at all on Mac OSX, without this mode switch.)

Mac OSX enforces this rule because it’s generally considered good interface design not to yank the mouse pointer away from the user’s direct control unless it is a special mode in which the user is likely to expect this behavior.

In general, I’d tend to agree. It’s probably a better design to have this camera mode be something the user activates explicitly, say by holding down the shift key or something, rather than on all the time. Certainly you don’t want this kind of mouse behavior active at the same time DirectGui buttons are also active, because they’re two so very different ways of using the mouse, and it’s likely to cause confusion or frustration if they’re both active at once. So you should consider having a modal switch that the user can activate to (a) turn on relative mouse mode and activate your camera interface, and (b) hide all of the onscreen DirectGui buttons.

David

I’m not sure what part of the design you don’t like. If you mean assigning a mouse button to camera controls and having clickable GUI on the same screen, then it’s like that in the original game we are porting. I’ve seen it in many games, including some Panda3d ones.
If you mean repositioning the mouse cursor and having clickable GUI on the same screen, then I agree, it might be a bad idea. Actually in the original game, the mouse cursor isn’t repositioned, however you can go out of the game window having the mouse button held down and the camera controls will still work.
If I could achieve this with Panda, I’d be more than glad as it would be identical to the original game and that’s what I’m aiming for.
However, I don’t know how.
Right now calculating the camera heading and pitch are easy:

self.heading = self.heading - (base.win.getPointer(0).getX() - base.win.getXSize()/2) * 0.5
self.pitch = self.pitch - (base.win.getPointer(0).getY() - base.win.getYSize()/2) * 0.5
base.camera.setHpr(self.heading, self.pitch,0)

How would you do that if you didn’t reposition the cursor to window center? (base.win.getXSize()/2, base.win.getYSize()/2).
It would probably involve

base.win.getPointer(0).getX()
base.win.getPointer(0).getY()

or

base.mouseWatcherNode.getMouseX()
base.mouseWatcherNode.getMouseY()

But then there’s my second issue, as the mouse can go outside of the window, so I would get an error message instead.

While the mouse button remains held down, the pointer will still belong to the window, and you will still get a value for getX() and getY(). It will just exceed the range -1 … 1. When the mouse button is released and the pointer is outside the window, then it will no longer return a value.

David

Oh I see. I didn’t check if it works while a mouse button is held down and was getting this

Assertion failed: _has_mouse at line 65 of c:\buildslave\dev_sdk_win32\build\pan
da3d\panda\src\tform\mouseWatcher.I
Traceback (most recent call last):
  File "test.py", line 6, in mouseTask
    print base.mouseWatcherNode.getMouseX()
AssertionError: _has_mouse at line 65 of c:\buildslave\dev_sdk_win32\build\panda
3d\panda\src\tform\mouseWatcher.I
:task(error): Exception occurred in PythonTask mouseTask
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    run()
  File "C:\Panda3D-1.8.0\direct\showbase\ShowBase.py", line 2630, in run
    self.taskMgr.run()
  File "C:\Panda3D-1.8.0\direct\task\Task.py", line 502, in run
    self.step()
  File "C:\Panda3D-1.8.0\direct\task\Task.py", line 460, in step
    self.mgr.poll()
  File "test.py", line 6, in mouseTask
    print base.mouseWatcherNode.getMouseX()
AssertionError: _has_mouse at line 65 of c:\buildslave\dev_sdk_win32\build\panda
3d\panda\src\tform\mouseWatcher.I

Any ideas how to calculate the heading and pitch values now though? I could try to post a small snippet on how it works right now.

Sure, just retain the previous pointer value each frame, and subtract the previous value from the current value (instead of subtracting the center value from the current value).

You should check base.mouseWatcherNode.hasMouse(), and ensure it returns true, before you call base.mouseWatcherNode.getMouseX(), to avoid the exception.

David

Got it, thanks.
I might post my camera manager class, i think it would be useful to others.

here it is

import direct.directbase.DirectStart
from pandac.PandaModules import *
from direct.task import Task

class CameraManager():
	"""
	CameraManager class.
	'lmb' - left mouse button
	'rmb' - right mouse button
	
	lmb + drag : rotate the camera
	lmb + rmb + drag : pan the camera
	rmb + drag : zoom the camera in/out
	"""
	
	def __init__(self, mode = 'thirdperson'):
		# camera orbit origin
		self.parentnode = render.attachNewNode('camparent')
		self.parentnode.reparentTo(render)
		self.parentnode.setEffect(CompassEffect.make(render)) # not inherit rotation
		
		base.camera.reparentTo(self.parentnode)
		base.camera.lookAt(self.parentnode)
		
		self.mode = mode
		self.setMode(self.mode)
		
		self.rotating = True
		self.panning = True
		self.zooming = True
		
		self.islmbdown = False
		self.isrmbdown = False
		
		base.accept("mouse1", self._leftMouseDown)
		base.accept("mouse1-up", self._leftMouseRelease)
		
		base.accept("mouse3", self._rightMouseDown)
		base.accept("mouse3-up", self._rightMouseRelease)
		
		# vars for camera rotation, panning and zooming
		self.heading = 0
		self.pitch = 0
		
		self.posx = 0
		self.posz = 0
		
		self.previousx = 0
		self.previousy = 0
		
		self.rotationsensitivity = 8.0
		self.panningsensitivity = 0.5
		self.zoomingsensitivity = 1.2

	def _leftMouseDown(self):
		self.previousx = base.win.getPointer(0).getX()
		self.previousy = base.win.getPointer(0).getY()
		
		self.islmbdown = True
	
	def _leftMouseRelease(self):
		self.previousx = base.win.getPointer(0).getX()
		self.previousy = base.win.getPointer(0).getY()
		
		self.islmbdown = False
		
	def _rightMouseDown(self):
		self.previousx = base.win.getPointer(0).getX()
		self.previousy = base.win.getPointer(0).getY()
		
		self.isrmbdown = True
		
	def _rightMouseRelease(self):
		self.previousx = base.win.getPointer(0).getX()
		self.previousy = base.win.getPointer(0).getY()
		
		self.isrmbdown = False
		
	def _cameraTask(self, task):
		self.x = base.win.getPointer(0).getX()
		self.y = base.win.getPointer(0).getY()
		
		# if lmb and rmb clicked, pan the camera
		if self.islmbdown == True and self.isrmbdown == True:
			if self.panning == True:			
				self.posx = self.posx + (self.previousx - self.x) * self.panningsensitivity * globalClock.getDt() # dt (delta time) ensures we get framerate independent movement
				self.posz = self.posz - (self.previousy - self.y) * self.panningsensitivity * globalClock.getDt()
				
				self.parentnode.setPos(self.parentnode, self.posx, 0, self.posz)
				
				# if we won't do this they will be added to the previous values
				self.posx = 0
				self.posz = 0
				
				self.previousx = self.x
				self.previousy = self.y
			else:
				pass
		
		# if lmb clicked, rotate the camera
		elif self.islmbdown == True:
			if self.rotating == True:
				self.heading = self.heading + (self.previousx - self.x) * self.rotationsensitivity * globalClock.getDt()
				self.pitch = self.pitch + (self.previousy - self.y) * self.rotationsensitivity * globalClock.getDt()
				
				if self.pitch > 90: self.pitch = 90
				elif self.pitch < -90: self.pitch = -90
					
				self.parentnode.setHpr(self.heading, self.pitch,0)
				
				self.previousx = self.x
				self.previousy = self.y
		
		# if rmb clicked, zoom the camera
		elif self.isrmbdown == True:
			if self.zooming == True:
				if self.y > self.previousy:
					self.posz = self.posz + (self.previousy - self.y) * self.zoomingsensitivity * globalClock.getDt()
					base.camera.setY(base.camera.getY() - self.posz)
				elif self.y < self.previousy:
					self.posz = self.posz - (self.previousy - self.y) * self.zoomingsensitivity * globalClock.getDt()
					base.camera.setY(base.camera.getY() + self.posz)
				
				self.previousy = self.y
				
				# if we won't do this they will be added to the previous values
				self.posx = 0
				self.posz = 0
			
		return task.cont
		
	def enableCamera(self):
		"""Enable CameraManager camera controls."""
		
		taskMgr.add(self._cameraTask, 'cameraTask')
		
	def disableCamera(self):
		"""Disable CameraManager camera controls."""
		
		taskMgr.remove('cameraTask')
		
	def resetCamera(self):
		"""Reset camera transforms."""
		
		self.parentnode.setPosHpr(0,0,0,0,0,0)
		base.cam.setY(-35)
		self.heading = 0
		self.pitch = 0
	
	def setTarget(self, target = render):
		"""Set target to rotate around."""
		
		self.parentnode.setPos(target.getPos())
		base.camera.reparentTo(self.parentnode)
	
	def setMode(self, mode):
		"""Choose between 1st and 3rd person camera modes."""
		
		if mode == 'thirdperson':
			base.camera.setY(-35) # camera distance from model
			self.mode = mode
		elif mode == 'firstperson':
			base.camera.setY(0)
			self.mode = mode
		else:
			print 'Invalid mode for setMode()'
			
	def setRotating(self, bool):
		"""Enable/disable camera rotation."""
		
		if bool == True:
			self.rotating = True
		elif bool == False:
			self.rotating = False
			
	def setPanning(self, bool):
		"""Enable/disable camera panning."""
		
		if bool == True:
			self.panning = True
		elif bool == False:
			self.panning = False
			
	def setZooming(self, bool):
		"""Enable/disable camera zooming."""
		
		if bool == True:
			self.zooming = True
		elif bool == False:
			self.zooming = False
				
	def setFov(self, value):
		"""Set the FOV (Field of View) of the default camera."""
		
		base.camLens.setFov(int(value))
	
	def setRotationSensitivity(self, value):
		"""Set rotation sensitivity for the mouse."""
		
		self.rotationsensitivity = float(value)
		
	def setPanningSensitivity(self, value):
		"""Set panning sensitivity for the mouse."""
		
		self.rotationsensitivity = float(value)
		
	def setZoomingSensitivity(self, value):
		"""Set zooming sensitivity for the mouse."""
		
		self.rotationsensitivity = float(value)
		
	def cleanup(self):
		"""Call this function before destroying the instance."""
		
		self.parentnode.removeNode()
		base.camera.reparentTo(render)
		base.ignore('wheel_up')
		base.ignore('wheel_down')
		base.ignore('mouse1')
		base.ignore('mouse1-up')
		base.ignore('mouse3')
		base.ignore('mouse3-up')

# example usage
camMgr = CameraManager()
base.disableMouse() # disable Panda's default camera controls and use our own
camMgr.enableCamera()