This is a small package that provides a blackbox camera creation and obj/group selection for RTS games or basically any game that uses a z=0 ground plane.
What I mean by blackbox is that you only have to add 2 lines of code to your program to get a camera that orbits with right click, edge pans, drag-select pan (with shift+leftmouse), group obj, individ/obj select, and ctrl select to add to an existing group. It only uses basic trig so it should be compatible with most (all?) programs. (The only problem is that it doesn’t do pixel perfect object selection, but most commericial games don’t either.)
It’s EXTREMELY easy to use so it’s good for newbies like me!
The original ideas came from piratepanda and Ninth’s rts camera discourse.panda3d.org/viewtopic … ht=camlens.
Usage: Save the code as GuiFuncs.py, and move to your progam directory.
Example 1:
In tut-5 of the solar system tutorial add:
w = world()
#new lines
from GuiFuncs import *
#Add the nodepaths that we want to be selectable to the selection tool
objSelectionTool.listConsideration = [w.moon, w.sun, w.mercury, w.mars, w.venus, w.earth]
run()
##(selected objs are placed in objSelectionTool.listSelected)
##rewrite clSelectionTool.func* to do your own selected item display
##Check guifuncs.py for behind the scenes stuff, main objs are
##builtin.objCameraHandler = clCameraHandler()
##builtin.objSelectionTool = clSelectionTool()
##builtin.objKeyBoardModifiers = clKeyBoardModifiers()
Example #2:
To add a new persepctive to ThomasEgi’s awesome cuboid clone discourse.panda3d.org/viewtopic.php?t=7506
In cuboid.py, comment out line 70
#taskMgr.add(self.cameraMovement,“CameraTask”)
Add
world()
from GuiFuncs import *
run()
import __builtin__
from direct.showbase import DirectObject
from direct.task import Task
import libpanda
from pandac.PandaModules import Vec4, CardMaker, LineSegs, Point2, Point3
import math
class clCameraHandler(DirectObject.DirectObject):
def __init__(self, tupCameraPos=(0, 0, 45), tupCameraDir=(0, -90, 0), tupThetaLimit=(5, 90), booDebug=False):
# Gives the camera an initial position and rotation.
self.booDebug = booDebug
self.booOrbiting = False
self.booDragPanning = False
self.fEdgeBound = 0.95
self.fLastMouseX, self.fLastMouseY = 0, 0
self.fDragPanRate = 30
self.fPanRate = 1
self.fPhiRate = 100
self.fThetaRate = 35
self.fZoomRate = 1.3
self.tupCurrentCameraPos = tupCameraPos
self.tupCurrentCameraDir = tupCameraDir
self.tupInitCameraDir = tupCameraDir
self.tupInitCameraPos = tupCameraPos
self.tupThetaLimit = tupThetaLimit
if not self.booDebug:
self.accept("mouse3", self.StartCameraOrbit)
self.accept("mouse3-up", self.StopCameraOrbit)
self.accept("wheel_down", self.ZoomCameraOut)
self.accept("wheel_up", self.ZoomCameraIn)
self.accept("shift-mouse1", self.StartDragPan)
self.accept("mouse1-up", self.StopDragPan)
taskMgr.add(self.CamTask,'CamTask')
self.GoHome()
def CamTask(self, task):
if base.mouseWatcherNode.hasMouse():
mpos = base.mouseWatcherNode.getMouse()
t_mpos_x = mpos.getX()
t_mpos_y = mpos.getY()
if (self.booOrbiting):
self.DoOrbit(t_mpos_x, t_mpos_y)
return task.cont
if (self.booDragPanning):
self.DoDragPan(t_mpos_x, t_mpos_y)
return task.cont
if (t_mpos_x < -0.9) | (t_mpos_x > 0.9) | (t_mpos_y < -0.9) | (t_mpos_y > 0.9):
#If i'm making a selection, don't pan the camera. It's very annoying.
try:
if objSelectionTool.booSelecting:
return task.cont
except:
pass
self.DoEdgePan(t_mpos_x, t_mpos_y)
return task.cont
return task.cont
def DoEdgePan(self, fMX, fMY): #MX, MY: mouse x, mouse y
dx, dy = 0, 0
old_Phi = 180 + self.tupCurrentCameraDir[0]
if fMX > self.fEdgeBound:
dx = math.cos(math.radians(old_Phi+180))*(fMX-self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
dy = math.sin(math.radians(old_Phi+180))*(fMX-self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
#print fMX, fMY, dx, dy
if fMX < -1*self.fEdgeBound:
dx = math.cos(math.radians(old_Phi+180))*(fMX+self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
dy = math.sin(math.radians(old_Phi+180))*(fMX+self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
#print fMX, fMY, dx ,dy
if fMY > self.fEdgeBound:
dx = -1*math.cos(math.radians(old_Phi+90))*(fMY-self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
dy = -1*math.sin(math.radians(old_Phi+90))*(fMY-self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
#print fMX, fMY, dx ,dy
if fMY < -1*self.fEdgeBound:
dx = -1*math.cos(math.radians(old_Phi+90))*(fMY+self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
dy = -1*math.sin(math.radians(old_Phi+90))*(fMY+self.fEdgeBound)/(1-self.fEdgeBound)*self.fPanRate/self.fZoomRate
#print fMX, fMY, dx ,dy
if not self.booDebug:
self.tupCurrentCameraPos = (self.tupCurrentCameraPos[0] + dx, self.tupCurrentCameraPos[1] + dy, self.tupCurrentCameraPos[2])
base.camera.setPos(self.tupCurrentCameraPos[0], self.tupCurrentCameraPos[1], self.tupCurrentCameraPos[2])
def DoDragPan(self, fMX, fMY):
dx, dy = 0, 0
old_Phi = 180 + self.tupCurrentCameraDir[0]
dMX = fMX - self.fLastMouseX
dMY = fMY - self.fLastMouseY
x_p = dMX*self.fDragPanRate/self.fZoomRate
y_p = dMY*self.fDragPanRate/self.fZoomRate
c_ = math.cos(math.radians(old_Phi+180))
s_ = math.sin(math.radians(old_Phi+180))
dx = -1*(c_*x_p - s_*y_p)
dy = -1*(s_*x_p + c_*y_p)
if self.booDebug:
print dx, dy
if not self.booDebug:
self.tupCurrentCameraPos = (self.tupCurrentCameraPos[0] + dx, self.tupCurrentCameraPos[1] + dy, self.tupCurrentCameraPos[2])
base.camera.setPos(self.tupCurrentCameraPos[0], self.tupCurrentCameraPos[1], self.tupCurrentCameraPos[2])
self.fLastMouseX = fMX
self.fLastMouseY = fMY
def DoOrbit(self, fMX, fMY):
if not self.booDebug:
dMX = fMX - self.fLastMouseX
dMY = fMY - self.fLastMouseY
else:
dMX = fMX
dMY = fMY
dTheta = -1*dMY * self.fThetaRate
dPhi = -1*dMX * self.fPhiRate
old_Theta = -1*self.tupCurrentCameraDir[1]
old_Phi = 180 + self.tupCurrentCameraDir[0]
old_xyz_r = self.tupCurrentCameraPos[2]/math.sin(abs(math.radians(self.tupCurrentCameraDir[1])))
old_xy_r = self.tupCurrentCameraPos[2]/math.tan(abs(math.radians(self.tupCurrentCameraDir[1])))
old_x = -1*old_xy_r*math.cos(math.radians(self.tupCurrentCameraDir[0]+90))
old_y = -1*old_xy_r*math.sin(math.radians(self.tupCurrentCameraDir[0]+90))
if self.booDebug:
print "Old target 2 cam theta", old_Theta
print "Old Camera pos", self.tupCurrentCameraPos
print "Old Camera dir", self.tupCurrentCameraDir
new_Theta = old_Theta + dTheta
if new_Theta < self.tupThetaLimit[0]:
new_Theta = 5
if new_Theta > self.tupThetaLimit[1]:
new_Theta = 90
new_Phi = old_Phi + dPhi
nz = 1.*old_xyz_r*math.sin(math.radians(new_Theta))
new_xy_r = 1.*old_xyz_r*math.cos(math.radians(new_Theta))
new_x = new_xy_r*math.cos(math.radians(new_Phi+90))
new_y = new_xy_r*math.sin(math.radians(new_Phi+90))
self.tupCurrentCameraPos = (self.tupCurrentCameraPos[0] + new_x - old_x, self.tupCurrentCameraPos[1] + new_y - old_y, nz)
self.tupCurrentCameraDir = (new_Phi - 180, -1*new_Theta, 0)
if self.booDebug:
print "New target 2 cam theta", new_Theta
print "New Camera Pos", self.tupCurrentCameraPos
print "New Camera dir", self.tupCurrentCameraDir
if not self.booDebug:
base.camera.setPos(self.tupCurrentCameraPos[0], self.tupCurrentCameraPos[1], self.tupCurrentCameraPos[2])
base.camera.setHpr(self.tupCurrentCameraDir[0], self.tupCurrentCameraDir[1], self.tupCurrentCameraDir[2])
self.fLastMouseX = fMX
self.fLastMouseY = fMY
def GoHome(self):
base.camera.setPos(self.tupInitCameraPos[0], self.tupInitCameraPos[1], self.tupInitCameraPos[2])
base.camera.setHpr(self.tupInitCameraDir[0], self.tupInitCameraDir[1], self.tupInitCameraDir[2])
self.tupCurrentCameraPos = self.tupInitCameraPos
self.tupCurrentCameraDir = self.tupInitCameraDir
def StartDragPan(self):
self.booDragPanning = True
if base.mouseWatcherNode.hasMouse():
mpos = base.mouseWatcherNode.getMouse()
self.fLastMouseX = mpos.getX()
self.fLastMouseY = mpos.getY()
def StopDragPan(self):
self.booDragPanning = False
def StartCameraOrbit(self):
self.booOrbiting = True
if base.mouseWatcherNode.hasMouse():
mpos = base.mouseWatcherNode.getMouse()
self.fLastMouseX = mpos.getX()
self.fLastMouseY = mpos.getY()
def StopCameraOrbit(self):
self.booOrbiting = False
def ZoomCameraOut(self):
old_Theta = -1*self.tupCurrentCameraDir[1]
old_Phi = 180 + self.tupCurrentCameraDir[0]
old_xyz_r = self.tupCurrentCameraPos[2]/math.sin(abs(math.radians(self.tupCurrentCameraDir[1])))
old_xy_r = self.tupCurrentCameraPos[2]/math.tan(abs(math.radians(self.tupCurrentCameraDir[1])))
old_x = -1*old_xy_r*math.cos(math.radians(self.tupCurrentCameraDir[0]+90))
old_y = -1*old_xy_r*math.sin(math.radians(self.tupCurrentCameraDir[0]+90))
new_xyz_r = old_xyz_r*self.fZoomRate
nz = 1.*new_xyz_r*math.sin(math.radians(old_Theta))
new_xy_r = 1.*new_xyz_r*math.cos(math.radians(old_Theta))
new_x = new_xy_r*math.cos(math.radians(old_Phi+90))
new_y = new_xy_r*math.sin(math.radians(old_Phi+90))
self.tupCurrentCameraPos = (self.tupCurrentCameraPos[0] + new_x - old_x, self.tupCurrentCameraPos[1] + new_y - old_y, nz)
base.camera.setPos(self.tupCurrentCameraPos[0], self.tupCurrentCameraPos[1], self.tupCurrentCameraPos[2])
def ZoomCameraIn(self):
old_Theta = -1*self.tupCurrentCameraDir[1]
old_Phi = 180 + self.tupCurrentCameraDir[0]
old_xyz_r = self.tupCurrentCameraPos[2]/math.sin(abs(math.radians(self.tupCurrentCameraDir[1])))
old_xy_r = self.tupCurrentCameraPos[2]/math.tan(abs(math.radians(self.tupCurrentCameraDir[1])))
old_x = -1*old_xy_r*math.cos(math.radians(self.tupCurrentCameraDir[0]+90))
old_y = -1*old_xy_r*math.sin(math.radians(self.tupCurrentCameraDir[0]+90))
new_xyz_r = old_xyz_r/self.fZoomRate
nz = 1.*new_xyz_r*math.sin(math.radians(old_Theta))
new_xy_r = 1.*new_xyz_r*math.cos(math.radians(old_Theta))
new_x = new_xy_r*math.cos(math.radians(old_Phi+90))
new_y = new_xy_r*math.sin(math.radians(old_Phi+90))
self.tupCurrentCameraPos = (self.tupCurrentCameraPos[0] + new_x - old_x, self.tupCurrentCameraPos[1] + new_y - old_y, nz)
base.camera.setPos(self.tupCurrentCameraPos[0], self.tupCurrentCameraPos[1], self.tupCurrentCameraPos[2])
class clKeyBoardModifiers(DirectObject.DirectObject):
def __init__(self):
self.booAlt = False
self.booControl = False
self.booShift = False
self.accept("alt", self.OnAltDown)
self.accept("alt-up", self.OnAltUp)
self.accept("control", self.OnControlDown)
self.accept("control-up", self.OnControlUp)
self.accept("shift", self.OnShiftDown)
self.accept("shift-up", self.OnShiftUp)
def OnAltDown(self):
self.booAlt = True
def OnAltUp(self):
self.booAlt = False
def OnControlDown(self):
self.booControl = True
def OnControlUp(self):
self.booControl = False
def OnShiftDown(self):
self.booShift = True
def OnShiftUp(self):
self.booShift = False
class clSelectionTool(DirectObject.DirectObject):
def __init__(self, listConsideration=[]):
#Create a selection window using cardmaker
#We will use the setScale function to dynamically scale the quad to the appropriate size in UpdateSelRect
temp = CardMaker('')
temp.setFrame(0, 1, 0, 1)
#self.npSelRect is the actual selection rectangle that we dynamically hide/unhide and change size
self.npSelRect = render2d.attachNewNode(temp.generate())
self.npSelRect.setColor(1,1,0,.2)
self.npSelRect.setTransparency(1)
self.npSelRect.hide()
LS = LineSegs()
LS.moveTo(0,0,0)
LS.drawTo(1,0,0)
LS.drawTo(1,0,1)
LS.drawTo(0,0,1)
LS.drawTo(0,0,0)
self.npSelRect.attachNewNode(LS.create())
self.listConsideration = listConsideration
self.listSelected = []
self.listLastSelected = []
self.pt2InitialMousePos = (-12, -12)
self.pt2LastMousePos = (-12, -12)
####----Used to differentiate between group selections and point selections
#self.booMouseMoved = False
self.fFovh, self.fFovv = base.camLens.getFov()
####--Used to control how frequently update_rect is updated;
self.fTimeLastUpdateSelRect = 0
self.fTimeLastUpdateSelected = 0
self.UpdateTimeSelRect = 0.015
self.UpdateTimeSelected = 0.015
####------Register the left-mouse-button to start selecting
self.accept("mouse1", self.OnStartSelect)
self.accept("control-mouse1", self.OnStartSelect)
self.accept("mouse1-up", self.OnStopSelect)
self.taskUpdateSelRect = 0
def TTest(self):
print "hello control-mouse1"
def funcSelectActionOnObject(self, obj):
obj.showBounds()
def funcDeselectActionOnObject(self, obj):
obj.hideBounds()
def OnStartSelect(self):
if not base.mouseWatcherNode.hasMouse():
return
self.booMouseMoved = False
self.booSelecting = True
self.pt2InitialMousePos = Point2(base.mouseWatcherNode.getMouse())
self.pt2LastMousePos = Point2(self.pt2InitialMousePos)
self.npSelRect.setPos(self.pt2InitialMousePos[0], 1, self.pt2InitialMousePos[1])
self.npSelRect.setScale(1e-3, 1, 1e-3)
self.npSelRect.show()
self.taskUpdateSelRect = taskMgr.add(self.UpdateSelRect, "UpdateSelRect")
self.taskUpdateSelRect.lastMpos = None
def OnStopSelect(self):
if not base.mouseWatcherNode.hasMouse():
return
if self.taskUpdateSelRect != 0:
taskMgr.remove(self.taskUpdateSelRect)
self.npSelRect.hide()
self.booSelecting = False
#If the mouse hasn't moved, it's a point selection
if (abs(self.pt2InitialMousePos[0] - self.pt2LastMousePos[0]) <= .01) & (abs(self.pt2InitialMousePos[1] - self.pt2LastMousePos[1]) <= .01):
objTempSelected = 0
fTempObjDist = 2*(base.camLens.getFar())**2
for i in self.listConsideration:
if type(i) != libpanda.NodePath:
raise 'Unknown objtype in selection'
else:
sphBounds = i.getBounds()
#p3 = base.cam.getRelativePoint(render, sphBounds.getCenter())
p3 = base.cam.getRelativePoint(i.getParent(), sphBounds.getCenter())
r = sphBounds.getRadius()
screen_width = r/(p3[1]*math.tan(math.radians(self.fFovh/2)))
screen_height = r/(p3[1]*math.tan(math.radians(self.fFovv/2)))
p2 = Point2()
base.camLens.project(p3, p2)
#If the mouse pointer is in the "roughly" screen-projected bounding volume
if (self.pt2InitialMousePos[0] >= (p2[0] - screen_width/2)):
if (self.pt2InitialMousePos[0] <= (p2[0] + screen_width/2)):
if (self.pt2InitialMousePos[1] >= (p2[1] - screen_height/2)):
if (self.pt2InitialMousePos[1] <= (p2[1] + screen_height/2)):
#We check the obj's distance to the camera and choose the closest one
dist = p3[0]**2+p3[1]**2+p3[2]**2 - r**2
if dist < fTempObjDist:
fTempObjDist = dist
objTempSelected = i
if objTempSelected != 0:
if objKeyBoardModifiers.booControl:
self.listSelected.append(objTempSelected)
else:
for i in self.listSelected:
self.funcDeselectActionOnObject(i)
self.listSelected = [objTempSelected]
self.funcSelectActionOnObject(objTempSelected)
def UpdateSelRect(self, task):
#Make sure we hae the mouse
if not base.mouseWatcherNode.hasMouse():
return Task.cont
mpos = base.mouseWatcherNode.getMouse()
t = globalClock.getRealTime()
#First check the mouse position is different
if self.pt2LastMousePos != mpos:
self.booMouseMoved = True
#We only need to check this function every once in a while
if (t - self.fTimeLastUpdateSelRect) > self.UpdateTimeSelRect:
self.fTimeLastUpdateSelRect = t
self.pt2LastMousePos = Point2(mpos)
#Update the selection rectange graphically
d = self.pt2LastMousePos - self.pt2InitialMousePos
self.npSelRect.setScale(d[0] if d[0] else 1e-3, 1, d[1] if d[1] else 1e-3)
if (abs(self.pt2InitialMousePos[0] - self.pt2LastMousePos[0]) > .01) & (abs(self.pt2InitialMousePos[1] - self.pt2LastMousePos[1]) > .01):
if (t - self.fTimeLastUpdateSelected) > self.UpdateTimeSelected:
#A better way to handle a large number of objects is to first transform the 2-d selection rect into
#its own view fustrum and then check the objects in world space. Adding space correlation/hashing
#will make it go faster. But I'm lazy.
self.fTimeLastUpdateSelected = t
self.listLastSelected = self.listSelected
self.listSelected = []
#Get the bounds of the selection box
fMouse_Lx = min(self.pt2InitialMousePos[0], self.pt2LastMousePos[0])
fMouse_Ly = max(self.pt2InitialMousePos[1], self.pt2LastMousePos[1])
fMouse_Rx = max(self.pt2InitialMousePos[0], self.pt2LastMousePos[0])
fMouse_Ry = min(self.pt2InitialMousePos[1], self.pt2LastMousePos[1])
for i in self.listConsideration:
if type(i) != libpanda.NodePath:
raise 'Unknown objtype in selection'
else:
#Get the loosebounds of the nodepath
sphBounds = i.getBounds()
#Put the center of the sphere into the camera coordinate system
#p3 = base.cam.getRelativePoint(render, sphBounds.getCenter())
p3 = base.cam.getRelativePoint(i.getParent(), sphBounds.getCenter())
#Check if p3 is in the view fustrum
p2 = Point2()
if base.camLens.project(p3, p2):
if (p2[0] >= fMouse_Lx) & (p2[0] <= fMouse_Rx) & (p2[1] >= fMouse_Ry) & (p2[1] <= fMouse_Ly):
self.listSelected.append(i)
self.funcSelectActionOnObject(i)
for i in self.listLastSelected:
if not objKeyBoardModifiers.booControl:
if i not in self.listSelected:
self.funcDeselectActionOnObject(i)
else:
self.listSelected.append(i)
return Task.cont
base.disableMouse() #disable mouse control of the camera
__builtin__.objCameraHandler = clCameraHandler()
__builtin__.objSelectionTool = clSelectionTool()
__builtin__.objKeyBoardModifiers = clKeyBoardModifiers()
if __name__ == "__main__":
a = clCameraHandler(booDebug=True)
a.fLastMouseX = 0
a.fLastMouseY = 0
a.DoDragPan(0, .5)