Stable FPS, with collision and physics

So, I’ve been around the panda block a bit, and after doing a lot of reading, and bashing my head against the wall I have a stable framework for a single-player FPS. I had hoped to make it network-able but networking is far beyond the scope of my python understanding. So, I thought I would share what I had with the community, for new player benefit, and for mine, should anyone have constructive criticism.

I know, it’s not pretty, I won’t pretend to be an artist, but, it’s a start and I hope something I can build off.

Here is the svn, should anyone care to snatch it that way.
dc-fps.googlecode.com/svn/trunk/

And for those who would like to just see the code:

## -*- coding: utf-8 -*-
from direct.directbase import DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenImage import OnscreenImage
from math import sqrt
import sys, time

from pandac.PandaModules import *
from direct.gui.DirectGui import *

class Main(DirectObject):

    def __init__(self):
        self.FLOORMASK=BitMask32.bit(0)
        self.WALLMASK=BitMask32.bit(1)
        self.accept("escape", sys.exit)

        #Make some light to see
        self.initLights()

        #Create the world
        self.colCount = 0
        self.world = loader.loadModel("./models/World.egg")
        self.world.reparentTo(render)
        self.world.setPos(0,0,0)
        self.world.setScale(2)

        #Ground Collision
        cGround = self.world.find("**/ground_col")
        cGround.node().setIntoCollideMask(self.FLOORMASK)

        #Wall collision
        cWall = self.world.find("**/wall_col")
        cWall.node().setIntoCollideMask(self.WALLMASK)

        #Set up the collision handlers
        self.initCollision()

        #Create the player model
        self.player = Player(self.FLOORMASK, self.WALLMASK)

        #Muck the mouse countrols and stuff
        props = WindowProperties()
        props.setCursorHidden(True)
        base.win.requestProperties(props)
        base.disableMouse()

        self.player.player.setH(base.camera.getH())
        base.camera.reparentTo(self.player.player)
        base.camera.setPos(0,0,0)
        self.player.player.setPos(0,0,10)

    def initCollision(self):
        """Setup the collision pushers and traverser"""
        #Generic traverser
        base.cTrav = CollisionTraverser('Collision Traverser')
        base.cTrav.setRespectPrevTransform(True)

        #Pusher Handler for walls
        base.cPush = PhysicsCollisionHandler()

        #Physics
        base.enableParticles()

        #Gravity
        self.gravityFN=ForceNode('world-forces')
        self.gravityFNP=render.attachNewNode(self.gravityFN)
        self.gravityForce=LinearVectorForce(0,0,-9.81)
        self.gravityFN.addForce(self.gravityForce)

        #Attach it to the global physics manager.
        base.physicsMgr.addLinearForce(self.gravityForce)

    def initLights(self):                    #Sets up some default lighting
        ambientLight = AmbientLight( "ambientLight" )
        ambientLight.setColor( Vec4(.4, .4, .35, 1) )
        directionalLight = DirectionalLight( "directionalLight" )
        directionalLight.setDirection( Vec3( 0, 8, -2.5 ) )
        directionalLight.setColor( Vec4( 0.9, 0.8, 0.9, 1 ) )
        render.setLight(render.attachNewNode( directionalLight ) )
        render.setLight(render.attachNewNode( ambientLight ) )

class Player(DirectObject):


    def __init__(self, floorMask, wallMask):
        """A Class for player specific controls"""
        self.FLOORMASK= floorMask
        self.WALLMASK= wallMask
        self.MouseSen = 0.1

        self.initPlayer()
        self.bullets = {}
        #imageObject = OnscreenImage(image = 'textures/DCL-Logo.bmp')
        #imageObject.setPos((-.82,0,.75))
        #imageObject.setScale((.5, 1,.25))
        self.initUI()
        self.frame = None

    def initUI(self):
        """Setup the players UI"""

        self.objText = OnscreenText(text = '', pos = (0,0,0), scale = 0.07)
        image = OnscreenImage(image = 'textures/crosshairs.png')
        image.setPos((0,0,0))
        image.setScale((0.08,1,0.08))
        image.setTransparency(TransparencyAttrib.MAlpha)

    def initPlayer(self):
        """Setup the player collision and camera setup"""

        #Player Node
        self.player = NodePath(ActorNode("Player"))
        self.player.reparentTo(render)

        #Player Movement
        self.vel = Vec3(0.0,0.0,0.0)
        self.strafespeed = 9.0
        self.forwardspeed = 11.0
        self.backspeed = 7.0

        #Player variables
        self.mouse = True

        #Add player controls
        self.keyMap = {"forward":0,"backward":0,"strafeleft":0,"straferight":0}
        self.accept("w",self.setKey, ["forward",1])
        self.accept("s",self.setKey, ["backward",1])
        self.accept("a",self.setKey, ["strafeleft",1])
        self.accept("d",self.setKey, ["straferight",1])
        self.accept("mouse1",self.mouse1down)
        self.accept("mouse2",self.mouse2down)
        self.accept("mouse2-up",self.mouse2up)
        self.accept("w-up",self.setKey, ["forward",0])
        self.accept("s-up",self.setKey, ["backward",0])
        self.accept("a-up",self.setKey, ["strafeleft",0])
        self.accept("d-up",self.setKey, ["straferight",0])

        #Collision handler for adding objects and the like
        self.pTrav = CollisionTraverser("Picker Traverser")
        self.pQueue = CollisionHandlerQueue()
        self.accept('bullet',self.removeBullet)
        base.cPush.addInPattern('%fn')

        #Picker collision
        pickerNode = CollisionNode('mouseRay')
        pickerNP = base.camera.attachNewNode(pickerNode)
        pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask())
        pickerNode.setIntoCollideMask(BitMask32().allOff())
        self.pickerRay = CollisionRay()
        pickerNode.addSolid(self.pickerRay)
        self.pTrav.addCollider(pickerNP, self.pQueue)

        #Player Collision Sphere
        self.cNode = CollisionNode("PlayerCollision")

        #To make the person tall, but not wide we use three collisionspheres
        self.cNode.addSolid(CollisionSphere(0,0,-1,1))
        self.cNode.addSolid(CollisionSphere(0,0,-3,1))
        self.cNode.addSolid(CollisionSphere(0,0,-5,1))

        self.cNodePath = self.player.attachNewNode(self.cNode)
        self.cNodePath.node().setFromCollideMask(self.FLOORMASK|self.WALLMASK)
        self.cNodePath.node().setIntoCollideMask(BitMask32.allOff())

        base.cPush.addCollider(self.cNodePath, self.player)
        base.cTrav.addCollider(self.cNodePath, base.cPush)

        #Attach it to the global physics manager.
        base.physicsMgr.attachPhysicalNode(self.player.node())

        taskMgr.add(self.updatePlayer, "updatePlayer")
        taskMgr.add(self.updateBullet, "Update Bullet")

    def setKey(self, key, value):
        self.keyMap[key] = value

    def updatePlayer(self, task):
        """Big task that updates the players position every tick"""
        elapsed = globalClock.getDt()

        self.objText['text'] = ''

        if self.mouse:
            if base.mouseWatcherNode.hasMouse():
                # get the current location. it's in the range (-1 to 1)
                md = base.win.getPointer(0)
                x = md.getX()
                y = md.getY()
                # rotate the camera based on the mouse coordinates
                if base.win.movePointer(0, base.win.getXSize()/2, base.win.getYSize()/2):
                    base.camera.setP((base.camera.getP() - (y-base.win.getYSize()/2)*self.MouseSen) % 360)
                    self.player.setH((self.player.getH() - (x-base.win.getXSize()/2)*self.MouseSen) % 360)

                mpos = base.mouseWatcherNode.getMouse()
                self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())
                self.pTrav.traverse(render)

                # Screen selection used to identify what the player is looking at
                if self.pQueue.getNumEntries() > 0:
                    self.pQueue.sortEntries()
                    entry = self.pQueue.getEntry(0)

                    eName = entry.getIntoNodePath().getName()
                    ePos = entry.getSurfacePoint(render)
                    self.newpos = ePos

                    distVec = ePos - self.player.getPos()
                    if distVec.length() < 12.0:
                        self.objText['text'] = eName
            x = 0.0
            y = 0.0
            #Move the player around
            if (self.keyMap["strafeleft"]!=0):
                x = -self.strafespeed
            if (self.keyMap["straferight"]!=0):
                x = self.strafespeed
            if (self.keyMap["forward"]!=0):
                y = self.forwardspeed
            if (self.keyMap["backward"]!=0):
                y = -self.backspeed

            self.vel = Vec3(x,y,0)
            self.vel *= elapsed
            self.player.setFluidPos(self.player, self.vel)

        return task.cont

    def mouse1down(self):
        """Fire a bullet, which at the moment is just a collision sphere"""
        fwd = base.camera.getQuat(render).getForward()
        b = Bullet(base.camera.getPos(render), fwd, 75.)
        self.bullets[b.cNodePath]=b

    def mouse2down(self):
        """Non-Toggle zoom"""
        base.camLens.setFov(30)

    def mouse2up(self):
        """Zoom back out"""
        base.camLens.setFov(40)

    def updateBullet(self, task):
        """Find all of the bullets in the game and update their position
            Would like to use a lerp interval, but don't know how with velocity
            instead of an end position"""
        elapsed = globalClock.getDt()

        for b in self.bullets.itervalues():
            p = b.bullet.getPos()
            v = b.vel

            b.bullet.setFluidPos(p+v*elapsed)

        return task.cont

    def removeBullet(self, entry):
        """Event that is raised when a bullet collides, remove it from the scene
        and the current list of bullets"""
        fnode = entry.getFromNodePath()
        try:
            b = self.bullets.pop(fnode)
            b.bullet.removeNode()
        except KeyError:
            pass

That was what I was after. Nice work.

Awesome, glad I could help.

I hope you don’t mind… I have stripped your code of all but the camera control, the code follows.

from direct.directbase import DirectStart 
from direct.showbase.DirectObject import DirectObject 
import sys, time 

from pandac.PandaModules import * 

class Player(DirectObject): 

    def __init__(self):
        #Player Movement 
        self.vel = Vec3(0.0,0.0,0.0) 
        self.strafespeed = 9.0 
        self.forwardspeed = 11.0 
        self.backspeed = 7.0 
        self.MouseSen = 0.1

        #Hide mouse
        props = WindowProperties() 
        props.setCursorHidden(True) 
        base.win.requestProperties(props) 
        base.disableMouse()

        #Create the world 
        self.world = loader.loadModel("./models/world") 
        self.world.reparentTo(render) 
        self.world.setPos(0,0,0) 
        self.world.setScale(2)

        #create the player
        self.player = NodePath(ActorNode("Player")) 
        self.player.reparentTo(render)
        self.player.setH(base.camera.getH()) 
        base.camera.reparentTo(self.player) 
        base.camera.setPos(0,0,0) 
        self.player.setPos(0,0,10) 

        #Grab input and run task
        self.accept("escape", sys.exit)
        self.keyMap = {"forward":0,"backward":0,"strafeleft":0,"straferight":0} 
        self.accept("e",self.setKey, ["forward",1]) 
        self.accept("f",self.setKey, ["backward",1]) 
        self.accept("a",self.setKey, ["strafeleft",1]) 
        self.accept("space",self.setKey, ["straferight",1]) 
        
        self.accept("e-up",self.setKey, ["forward",0]) 
        self.accept("f-up",self.setKey, ["backward",0]) 
        self.accept("a-up",self.setKey, ["strafeleft",0]) 
        self.accept("space-up",self.setKey, ["straferight",0])

        taskMgr.add(self.updatePlayer, "updatePlayer") 

        

        

    def setKey(self, key, value): 
        self.keyMap[key] = value 

    def updatePlayer(self, task): 
        """Big task that updates the players position every tick""" 
        elapsed = globalClock.getDt() 

        if base.mouseWatcherNode.hasMouse():
            # get mouse coords
            md = base.win.getPointer(0) 
            x = md.getX() 
            y = md.getY() 
            # rotate the camera based on the mouse coordinates 
            if base.win.movePointer(0, base.win.getXSize()/2, base.win.getYSize()/2): 
                base.camera.setP((base.camera.getP() - (y-base.win.getYSize()/2)*self.MouseSen) % 360) 
                self.player.setH((self.player.getH() - (x-base.win.getXSize()/2)*self.MouseSen) % 360) 

            # reset mouse coords for next iteration
            x = 0.0 
            y = 0.0 
            #Move the player around 
            if (self.keyMap["strafeleft"]!=0): 
                x = -self.strafespeed 
            if (self.keyMap["straferight"]!=0): 
                x = self.strafespeed 
            if (self.keyMap["forward"]!=0): 
                y = self.forwardspeed 
            if (self.keyMap["backward"]!=0): 
                y = -self.backspeed 

            self.vel = Vec3(x,y,0) 
            self.vel *= elapsed 
            self.player.setFluidPos(self.player, self.vel) 

        return task.cont 

    


play=Player()

run() 

Shorter is always better, certainly don’t need everything I had.

Nice, I will test at home. This might be just what i was looking for to help me in a research project.
Taking a look at the code I am wondering, wouldn’t it be better if the bullets where a class themselves?

I’ve done that, but the class didn’t do much, and I didn’t need it do anything special. If there was a way to do a LerpInterval using initial position and initial velocity, then sure a class would be better, but I haven’t found a way to do that. Aside from that, Im not sure what benefits a class would bring.

Rokmonkey, thank you so much for this code. I highly appreciate it.