1st person camera, "free view" style

Use the wsad keys to move and mouse to look around

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

base.disableMouse()

# hide mouse cursor, comment these 3 lines to see the cursor
props = WindowProperties()
props.setCursorHidden(True)
base.win.requestProperties(props)

# dummy node for camera, attach player to it
camparent = render.attachNewNode('camparent')
camparent.reparentTo(render) # inherit transforms
camparent.setEffect(CompassEffect.make(render)) # NOT inherit rotation

# the camera
base.camera.reparentTo(camparent)
base.camera.lookAt(camparent)
base.camera.setY(0) # camera distance from model

# vars for camera rotation
heading = 0
pitch = 0

# camera rotation task
def cameraTask(task):
	global heading
	global pitch
	
	md = base.win.getPointer(0)
	 
	x = md.getX()
	y = md.getY()
	
	if base.win.movePointer(0, 300, 300):
		heading = heading - (x - 300) * 0.2
		pitch = pitch - (y - 300) * 0.2
	
	camparent.setHpr(heading, pitch, 0)
	
	if forward == True:
		camparent.setY(base.cam, camparent.getY(base.cam) + 2)
	if reverse == True:
		camparent.setY(base.cam, camparent.getY(base.cam) - 2)
	if left == True:
		camparent.setX(base.cam, camparent.getX(base.cam) - 2)
	if right == True:
		camparent.setX(base.cam, camparent.getX(base.cam) + 2)
   
	return task.cont

taskMgr.add(cameraTask, 'cameraTask')

# movement
forward = False
reverse = False
left = False
right = False

def forward():
	global forward
	forward = True
			
def stopForward():
	global forward
	forward = False

def reverse():
	global reverse
	reverse = True
			
def stopReverse():
	global reverse
	reverse = False

def left():
	global left
	left = True
			
def stopLeft():
	global left
	left = False

def right():
	global right
	right = True
			
def stopRight():
	global right
	right = False

base.accept("mouse3", forward)
base.accept("mouse3-up", forward)
base.accept("w", forward)
base.accept("w-up", stopForward)
base.accept("s", reverse)
base.accept("s-up", stopReverse)
base.accept("a", left)
base.accept("a-up", stopLeft)
base.accept("d", right)
base.accept("d-up", stopRight)

That seems like abuse of Python’s global scope. Why not create a camera controller class? That would be much more usable when integrating it into a game.

Secondly, why not poll for key presses instead of using events this way? That would greatly reduce the amount of code.

its sample :astonished:

IMHO, even samples should use good coding practices.

I second that. I don’t want to be rude (although it will probably sound that way), but I don’t think anyone can learn anything valuable from this code. The contrary, in fact…

it did end up rude :confused: I learn from critisism and will try to give my objective opinion anyway:
The point of this snippet is to show people how to implement 1st person camera with rotation, movement and panning without any real math, by using the parent/child relations in Panda. That’s it.

From what I’ve learned any “global” declaration in a function is pretty much “abuse” of Python’s global scope, the most common “workarounds” are to use dictionary or class as container for your shared variables or just have your whole code in a class.

class Globals:
    pass

Globals.a = 1

def myFunction():
    Globals.a = 2

myFunction()
print Globals.a

or only declare dictionary global

globals = {}

globals['a'] = 1

def myFunction():
    global globals
    globals['a'] = 2

myFunction()
print globals['a'] 

Others suggest to wrap everything you have in a class and create a single instance just to add an extra scope.
It’s my opinion that for a small snippet like this which is not intended to be used out-of-the-box, it’s not worth it. Every time this kind of “accessing local variables from elsewhere” discussion pops-up, it turns into some kind of religious war where people argue which one of the above methods is better without any good points.

From the other similar camera snippets Ive seen here I dont see how mine provides less info (‘anything valuable’) and seeing how short snippets like Blender scripts make use of “global”, I dont see how it’s a big deal for such a small snippet. I personally haven’t used any snippets from here out-of-the-box and end up just learning from them and adding to my own code.

Ive found the Panda3d forums to be one of the nicest programming forums and although I’m not offended, I’d still suggest you to be a bit more nice and not jump straight to the “criticism”, if it were someone else, he could have just read these posts as complaints on how bad his work is and you could lose another member. I am trying to be helpful when posting my own code to public, I dont get anything from it, so a “thanks” or “nice try” wouldn’t hurt.

I’m sorry that you were offended, but I did try to give constructive criticism. If you post a snippet in the forums, you should be prepared to receive constructive criticism and learn from that.

Sorry. You said that you interpreted it as rude, so I assumed you were offended.

How about:

a = 1
def myFunction(a):
    a = 2

That won’t work if you want to change the global variable “a” in a function.

Ouch - I didn’t catch that. This will work, on the other hand:

>>> class Dummy(object):
...     def __init__(self):
...             self.var = 5
... 
>>> d = Dummy()
>>> def my_func(cls):
...     cls.var = 4
... 
>>> my_func(d)
>>> d.var
4

Modifying a class inside a function it was passed to is almost never a good idea; that’s just asking for lots of hard-to-find, hidden bugs.

I’m not sure I understand you, but I don’t see a problem in modifying class attributes in a function, if you are using the class merely for globals. It’s either that or using the keyword global. Plus it adds an extra scope so they are not really “globals”.
Of course All I mention here is in the situation where you really don’t want to wrap all your code in classes.

Why use globals at all?
Write a proper class that does one thing and it’s good.
No namespace pollution, no random stuff lurking around.

I don’t think I can answer “Why use procedural oriented programming in Python at all?”, all I can say Python doesn’t force you to wrap all your code in classes. Many people including myself do use them, but for those who don’t for whatever reason, that’s the few ways I see of doing this.

thanks for the code post, this is a nice alternative to diving straight in to some trig, etc. I’m new to panda3d sdk so taking advantage of the inherent panda functions is quite nice. can you explain what the code is doing exactly? it appears you attach a node to the camera node and then perform follow type functions to it (lookat)

also, why only set y here: “base.camera.setY(0) # camera distance from model”

thanks for the contribution!

This will run differently depending on the speed of the users PC if I read it correctly (albeit briefly). Better to base movement on time since last frame?

Cheers,
Gary

Here is an earlier thread which includes an implementation of mouse control as a new class:
[Controlling Camera + Tips for Panda-newbie)

Maybe a good source of inspiration.

Cheers

I combined several code snippets I could find and came up with my “own” version of a mouse look class. It seems to work fine. The problem I have is when looking using mouse up/down and left/right together the camera starts to rotate and I don’t know why. Maybe someone of you knows?

The class:

from pandac.PandaModules import * 
from direct.task import Task 
from direct.showbase.DirectObject import DirectObject 

class MouseLook (DirectObject):
	"""
	simple camera mouse look and WASD key control
	r and f keys move camera up/down
	q and e keys rotate camera	
	hit enter to start/stop controls
	"""

	def __init__(self,camera):
	
		self.camera  = camera
		self.running = False
		self.time    = 0
		self.centX   = base.win.getProperties().getXSize()/2
		self.centY   = base.win.getProperties().getYSize()/2

		# key controls
		self.forward   = False 
		self.backward  = False 
		self.left      = False 
		self.right     = False 
		self.up        = False 
		self.down      = False 
		self.up        = False 
		self.down      = False 
		self.rollLeft  = False 
		self.rollRight = False 

		# sensitivity settings
		self.movSens  = 2
		self.rollSens = 50
		self.sensX = self.sensY = 0.2		
		
		self.accept("enter",self.toggle) 			

	# camera rotation task 
	def cameraTask(self,task):
		dt = task.time - self.time
		
		# handle mouse look
		md = base.win.getPointer(0) 	   
		x = md.getX() 
		y = md.getY() 
		
		if base.win.movePointer(0, self.centX, self.centY):	
			self.camera.setH(self.camera,self.camera.getH(self.camera) - (x - self.centX) * self.sensX)
			self.camera.setP(self.camera,self.camera.getP(self.camera) - (y - self.centY) * self.sensY)		

		# handle keys:

		if self.forward == True: 
			self.camera.setY(self.camera, self.camera.getY(self.camera) + self.movSens*dt) 
		if self.backward == True: 
			self.camera.setY(self.camera, self.camera.getY(self.camera) - self.movSens*dt) 
		if self.left == True: 
			self.camera.setX(self.camera, self.camera.getX(self.camera) - self.movSens*dt) 
		if self.right == True: 
			self.camera.setX(self.camera, self.camera.getX(self.camera) + self.movSens*dt) 
		if self.up == True: 
			self.camera.setZ(self.camera, self.camera.getZ(self.camera) + self.movSens*dt) 
		if self.down == True: 
			self.camera.setZ(self.camera, self.camera.getZ(self.camera) - self.movSens*dt) 			
		if self.rollLeft == True: 
			self.camera.setR(self.camera, self.camera.getR(self.camera) - self.rollSens*dt) 
		if self.rollRight == True: 
			self.camera.setR(self.camera, self.camera.getR(self.camera) + self.rollSens*dt) 
			
		self.time = task.time		
		return task.cont 

	def start(self):	
		base.disableMouse() 
		# hide mouse cursor, comment these 3 lines to see the cursor 
		props = WindowProperties() 
		props.setCursorHidden(True) 
		base.win.requestProperties(props) 
		# reset mouse to start position:
		base.win.movePointer(0, self.centX, self.centY)				
		taskMgr.add(self.cameraTask, 'HxMouseLook::cameraTask') 		
		#Task for changing direction/position 
		self.accept("w",setattr,[self,"forward",True]) 
		self.accept("w-up",setattr,[self,"forward",False]) 
		self.accept("s",setattr,[self,"backward",True]) 
		self.accept("s-up",setattr,[self,"backward",False]) 
		self.accept("a",setattr,[self,"left",True]) 
		self.accept("a-up",setattr,[self,"left",False]) 
		self.accept("d",setattr,[self,"right",True]) 
		self.accept("d-up",setattr,[self,"right",False]) 
		self.accept("r",setattr,[self,"up",True]) 
		self.accept("r-up",setattr,[self,"up",False]) 
		self.accept("f",setattr,[self,"down",True]) 
		self.accept("f-up",setattr,[self,"down",False]) 
		self.accept("q",setattr,[self,"rollLeft",True]) 
		self.accept("q-up",setattr,[self,"rollLeft",False]) 
		self.accept("e",setattr,[self,"rollRight",True]) 
		self.accept("e-up",setattr,[self,"rollRight",False]) 
		
	def stop(self):
		taskMgr.remove("HxMouseLook::cameraTask") 

		base.enableMouse()
		props = WindowProperties() 
		props.setCursorHidden(False) 
		base.win.requestProperties(props) 		
		
		self.forward  = False 
		self.backward = False 
		self.left     = False 
		self.right    = False 
		self.up       = False 
		self.down     = False 
		self.rollLeft = False 

		self.ignore("w") 
		self.ignore("w-up") 
		self.ignore("s") 
		self.ignore("s-up") 
		self.ignore("a") 
		self.ignore("a-up") 
		self.ignore("d") 
		self.ignore("d-up") 
		self.ignore("r") 
		self.ignore("r-up") 
		self.ignore("f") 
		self.ignore("f-up") 
		self.ignore("q") 
		self.ignore("q-up") 
		self.ignore("e") 
		self.ignore("e-up") 				
		
	#call to stop control system 
	def toggle(self): 
		if(self.running):
			self.stop()
			self.running=False
		else:
			self.start() 
			self.running=True

Example of usage:

from direct.showbase.ShowBase import ShowBase
from MouseLook import *
 
class MyApp(ShowBase):

	def __init__(self):
		ShowBase.__init__(self)

		# Load the environment model.
		self.environ = self.loader.loadModel("models/environment")
		# Reparent the model to render.
		self.environ.reparentTo(self.render)
		# Apply scale and position transforms on the model.
		self.environ.setScale(0.25, 0.25, 0.25)
		self.environ.setPos(-8, 42, 0)
		
		self.mouseLook = MouseLook(base.cam)


app = MyApp()
app.run()

I made some little modification to RootDwarf’s code.
Here is the code with some comments (I renamed the class in FirstPersonCamera and saved it in a module called FirstPersonCamera.py). To create the camera controller you need to pass a nodepath containing the camera and if a refNode is specified, heading and up/down are performed wrt this node; so if it is the root node of scene (i.e. base.render) “looking around” is done in a more acceptable way, moreover with shift pressed it goes faster.

from direct.showbase import DirectObject
#from pandac.PandaModules import WindowProperties
#from panda3d.core import CollisionHandlerPusher, CollisionNode, \
#                         CollisionSphere
from direct.task import Task 

##   First person camera controller, "free view"/"FPS" style.
#    
#    Simple camera mouse look and WASD key controller 
#    shift to go faster,
#    r and f keys move camera up/down, 
#    q and e keys rotate camera,
#    hit enter to start/stop controls.
#    If a refNode is specified, heading and up/down are performed wrt the 
#    reference node (usually the root node of scene, i.e. base.render)
#    and camera behaves more similarly to an "FPS" camera.
class FirstPersonCamera(DirectObject.DirectObject):
    '''
    First person camera controller.
    '''
    
    ## Constructor
    # @param gameaApp: the game application to which this controller
    # applies, that should be ShowBase derived.
    # @param camera: the camera to which this controller applies
    # @param refNode: reference node wrt heading and up/down are performed
    def __init__(self, gameApp, camera, refNode=None,
                 collisionHandler=None):
        '''
        Constructor
        '''
        
        self.gameApp = gameApp
        self.camera = camera
        if refNode != None:
            self.refNode = refNode
        else:
            self.refNode = self.camera
        self.running = False 
        self.time = 0 
        self.centX = self.gameApp.win.getProperties().getXSize() / 2 
        self.centY = self.gameApp.win.getProperties().getYSize() / 2 
        
        # key controls 
        self.forward = False 
        self.backward = False 
        self.fast = 1.0 
        self.left = False 
        self.right = False 
        self.up = False 
        self.down = False 
        self.up = False 
        self.down = False 
        self.rollLeft = False 
        self.rollRight = False 
        
        # sensitivity settings 
        self.movSens = 2
        self.movSensFast = self.movSens * 5
        self.rollSens = 50 
        self.sensX = self.sensY = 0.2       
        
        self.collisionHandler = collisionHandler
        self.collideMask = BitMask32(0x10)

        #press enter to get this camera controller
        self.accept("enter", self.toggle)           

    ## Get camera collide mask
    def getCollideMask(self):
        return self.collideMask
    
    ## Camera rotation task 
    def cameraTask(self, task): 
        dt = task.time - self.time 
         
        # handle mouse look 
        md = self.gameApp.win.getPointer(0)        
        x = md.getX() 
        y = md.getY() 
         
        if self.gameApp.win.movePointer(0, self.centX, self.centY):    
            self.camera.setH(self.refNode, self.camera.getH(self.refNode) 
                             - (x - self.centX) * self.sensX) 
            self.camera.setP(self.camera, self.camera.getP(self.camera) 
                             - (y - self.centY) * self.sensY)       
        
        # handle keys: 
        if self.forward == True: 
            self.camera.setY(self.camera, self.camera.getY(self.camera) 
                             + self.movSens * self.fast * dt)
        if self.backward == True:
            self.camera.setY(self.camera, self.camera.getY(self.camera) 
                             - self.movSens * self.fast * dt) 
        if self.left == True:
            self.camera.setX(self.camera, self.camera.getX(self.camera) 
                             - self.movSens * self.fast * dt) 
        if self.right == True:
            self.camera.setX(self.camera, self.camera.getX(self.camera) 
                             + self.movSens * self.fast * dt) 
        if self.up == True:
            self.camera.setZ(self.refNode, self.camera.getZ(self.refNode) 
                             + self.movSens * self.fast * dt) 
        if self.down == True:
            self.camera.setZ(self.refNode, self.camera.getZ(self.refNode) 
                             - self.movSens * self.fast * dt)           
        if self.rollLeft == True:
            self.camera.setR(self.camera, self.camera.getR(self.camera) 
                             - self.rollSens * dt)
        if self.rollRight == True:
            self.camera.setR(self.camera, self.camera.getR(self.camera) 
                             + self.rollSens * dt)
            
        self.time = task.time       
        return Task.cont 

    ## Start to control the camera
    def start(self):
        self.gameApp.disableMouse()
        self.camera.setP(self.refNode, 0)
        self.camera.setR(self.refNode, 0)
        # hide mouse cursor, comment these 3 lines to see the cursor 
        props = WindowProperties() 
        props.setCursorHidden(True) 
        self.gameApp.win.requestProperties(props) 
        # reset mouse to start position: 
        self.gameApp.win.movePointer(0, self.centX, self.centY)             
        self.gameApp.taskMgr.add(self.cameraTask, 'HxMouseLook::cameraTask')        
        #Task for changing direction/position 
        self.accept("w", setattr, [self, "forward", True])
        self.accept("shift-w", setattr, [self, "forward", True])
        self.accept("w-up", setattr, [self, "forward", False]) 
        self.accept("s", setattr, [self, "backward", True]) 
        self.accept("shift-s", setattr, [self, "backward", True]) 
        self.accept("s-up", setattr, [self, "backward", False])
        self.accept("a", setattr, [self, "left", True]) 
        self.accept("shift-a", setattr, [self, "left", True]) 
        self.accept("a-up", setattr, [self, "left", False]) 
        self.accept("d", setattr, [self, "right", True]) 
        self.accept("shift-d", setattr, [self, "right", True]) 
        self.accept("d-up", setattr, [self, "right", False]) 
        self.accept("r", setattr, [self, "up", True])
        self.accept("shift-r", setattr, [self, "up", True]) 
        self.accept("r-up", setattr, [self, "up", False])
        self.accept("f", setattr, [self, "down", True])
        self.accept("shift-f", setattr, [self, "down", True])
        self.accept("f-up", setattr, [self, "down", False]) 
        self.accept("q", setattr, [self, "rollLeft", True]) 
        self.accept("q-up", setattr, [self, "rollLeft", False]) 
        self.accept("e", setattr, [self, "rollRight", True]) 
        self.accept("e-up", setattr, [self, "rollRight", False])
        self.accept("shift", setattr, [self, "fast", 10.0])
        self.accept("shift-up", setattr, [self, "fast", 1.0])
        # setup collisions
        # setup collisions
        if self.collisionHandler != None:
            #setup collisions
            nearDist = self.camera.node().getLens().getNear()
            # Create a collision node for this camera.
            # and attach it to the camera.
            self.collisionNP = self.camera.attachNewNode(CollisionNode("firstPersonCamera"))
            # Attach a collision sphere solid to the collision node.
            self.collisionNP.node().addSolid(CollisionSphere(0, 0, 0, nearDist * 1.1))
#            self.collisionNP.show()
            # setup camera "from" bit-mask
            self.collisionNP.node().setFromCollideMask(self.collideMask)
            # add to collisionHandler (Pusher)
            self.collisionHandler.addCollider(self.collisionNP, self.camera)
            #add camera to collision system
            self.gameApp.cTrav.addCollider(self.collisionNP, self.collisionHandler)

    ## Stop to control the camera  
    def stop(self): 
        self.gameApp.taskMgr.remove("HxMouseLook::cameraTask") 
        
        mat = LMatrix4f(self.camera.getTransform(self.refNode).getMat())
        mat.invertInPlace()
        self.camera.setMat(LMatrix4f.identMat())
        self.gameApp.mouseInterfaceNode.setMat(mat)
        self.gameApp.enableMouse() 
        props = WindowProperties() 
        props.setCursorHidden(False) 
        self.gameApp.win.requestProperties(props)        
         
        self.forward = False 
        self.backward = False 
        self.left = False 
        self.right = False 
        self.up = False 
        self.down = False 
        self.rollLeft = False 
        
        self.ignore("w") 
        self.ignore("shift-w") 
        self.ignore("w-up") 
        self.ignore("s") 
        self.ignore("shift-s") 
        self.ignore("s-up") 
        self.ignore("a")
        self.ignore("shift-a") 
        self.ignore("a-up") 
        self.ignore("d")
        self.ignore("shift-d") 
        self.ignore("d-up") 
        self.ignore("r")
        self.ignore("shift-r")
        self.ignore("r-up") 
        self.ignore("f")
        self.ignore("shift-f")
        self.ignore("f-up") 
        self.ignore("q") 
        self.ignore("q-up") 
        self.ignore("e") 
        self.ignore("e-up")
        self.ignore("shift")
        self.ignore("shift-up")             
        # un-setup collisions
        if self.collisionHandler != None:
            # remove camera from the collision system
            self.gameApp.cTrav.removeCollider(self.collisionNP)
            # remove from collisionHandler (Pusher)
            self.collisionHandler.removeCollider(self.collisionNP)
            # remove the collision node
            self.collisionNP.removeNode() 
       
    ## Call to start/stop control system 
    def toggle(self): 
        if(self.running): 
            self.stop() 
            self.running = False 
        else: 
            self.start() 
            self.running = True 

and here an example on how to use it


from direct.showbase.ShowBase import ShowBase
#import from FirstPersonCamera.py
from FirstPersonCamera import FirstPersonCamera

## Main game class.
class TestCamera(ShowBase):
    '''
    Main TestCamera class.
    '''

    def __init__(self):
        '''
        Constructor
        '''
        ShowBase.__init__(self)

        # Load the environment model. 
        self.environ = self.loader.loadModel("models/environment") 
        # Reparent the model to render. 
        self.environ.reparentTo(self.render) 
        # Apply scale and position transforms on the model. 
        self.environ.setScale(0.25, 0.25, 0.25) 
        self.environ.setPos(-8, 42, 0) 
         
        self.mouseLook = FirstPersonCamera(self, self.cam, self.render)         
          

if __name__ == '__main__':
    gameApp = TestCamera()
    gameApp.run()