I am new to Panda.
Before starting any complex game, I am trying to write something like a 3-D pacman-like game as a starting point.
I managed to do joystick integration and I am able to move the avatar around, and the collision detection works almost like I expected.
I adopted the code from the collision detection example (Tut-labyrinth.py) for that purpose.
However, in my program the collision detection seems to give slightly incorrect results that make the avatar overlap the walls slightly or pushed too far from the wall. The visible effect is that if try to move the avatar against a wall, it seems to “tremble”.
For example, my program logs the following output:
The interpretation is:
I am trying to move the avatar upwards (increase y position),
and the new position is Point3(18.5, 8.56838, 0.5)
The avatar itself (the CollisionSphere around its center) has a radius of 0.5, thus the avatar’s maximum y-position is 9.06838.
Then a collision is detected against the wall (in x-direction) at y-position 5, which is correct.
But the interior point is not Point3(18.5, 8.56838, 0.5), as expected, but Point3(18.5, 9.01986, 0.5) instead.
Thus the computed depth (as in the Tut-labyrinth.py example) is 0.01985 and not 0.06838 as expected.
The depth is used for the avatar’s displacement then, resulting in a new posisiton Point3(18.5, 8.54852, 0.5) and instead of the correct value Point3(18.5, 8.5, 0.5).
As a result, the avatar is visibly intersecting the wall.
As opposed to the sample program, in my program the wall vertexes and triangles are not loaded from an egg file, but computed dynamically at program startup.
The code follows:
File name: level1.maz
Content:
**************************
* * *
* * ****** ** *
* * *
* ********** ******
* * * * *
* * * **** S * *
* * ***** *** * *
* * * *
* * * * *** *
* *
**************************
File name: main.py
Content:
#!/bin/env python
# -*- coding: iso-8859-1 -*-
import direct.directbase.DirectStart #Initialize Panda and create a window
from pandac.PandaModules import * #Contains most of Panda's modules
from direct.gui.DirectGui import * #Imports Gui objects we use for putting
#text on the screen
from direct.interval.IntervalGlobal import *
from direct.showbase.DirectObject import messenger, DirectObject
from direct.task import Task
from direct.actor import Actor
import sys
import pygame
from maze2d import Maze2D
from smiley import Avatar
class World(DirectObject): #Our main class
def showCamPos(self, task):
pos = camera.getPos()
self.title.setText ("X=%6.1f Y=%6.1f Z=%6.1f" % (pos[0], pos[1], pos[2]))
return Task.cont
def initJoystick(self, mappings):
self.joystickMappings = mappings
#Import pygame and init
pygame.init()
#Setup and init joystick(s)
for avatar, joyno in mappings.items():
j=pygame.joystick.Joystick(joyno)
j.init()
#Check init status
if j.get_init() == 1: print "Joystick %d is initialized" % joyno
"""
#Get and print joystick ID
print "Joystick ID: ", j.get_id()
#Get and print joystick name
print "Joystick Name: ", j.get_name()
#Get and print number of axes
print "No. of axes: ", j.get_numaxes()
#Get and print number of trackballs
print "No. of trackballs: ", j.get_numballs()
#Get and print number of buttons
print "No. of buttons: ", j.get_numbuttons()
#Get and print number of hat controls
print "No. of hat controls: ", j.get_numhats()
"""
avatar.joystick = j
def joystickWatcher(self, task):
"""
Leitet Joystick-Events weiter an die Avatare
"""
#Standard technique for finding the amount of time since the last frame
dt = task.time - task.last
task.last = task.time
#If dt is large, then there has been a #hiccup that could cause the ball
#to leave the field if this functions runs, so ignore the frame
if dt > .2: return Task.cont
def axisMotion (j, axis):
"Hilfsfunktion"
val = j.get_axis(axis)
if abs(val) > 0.1:
return val
return 0.0
for e in pygame.event.get():
pass
for avatar in self.joystickMappings.keys():
if avatar.wantsToMoveX or avatar.wantsToMoveY:
pass #Steuerung ist schon über die Tastatur erfolgt
else:
j = avatar.joystick
dx = +avatar.speed * dt * axisMotion (j, 0)
dy = -avatar.speed * dt * axisMotion (j, 1)
avatar.wantsToMoveX = dx
avatar.wantsToMoveY = dy
if dx or dy:
print "dt:", dt
return Task.cont
def __init__(self): #The initialization method caused when a
#world object is created
#Create some text overlayed on our screen.
#We will use similar commands in all of our tutorials to create titles and
#instruction guides.
self.title = OnscreenText(
text="Hennings erster Test",
style=1, fg=(1,1,1,1), pos=(0.8,-0.95), scale = .07,
mayChange=True)
#Make the background color black (R=0, B=0, G=0)
#instead of the default grey
base.setBackgroundColor(0, 0, 0)
#By default, the mouse controls the camera. Often, we disable that so that
#the camera can be placed manually (if we don't do this, our placement
#commands will be overridden by the mouse control)
base.disableMouse()
#base.useDrive()
#base.useTrackball()
#base.enableMouse()
taskMgr.add(self.showCamPos, "showCamPos", priority=3)
# Collision Detection vorbereiten
#initialize traverser
base.cTrav = CollisionTraverser()
#base.cTrav.showCollisions(render)
"""
pandaActor = Actor.Actor("models/panda-model",{"walk":"models/panda-walk4"})
pandaActor.setScale(0.01,0.01,0.01)
npLeine = render.attachNewNode("leine")
npLeine.reparentTo(render)
npLeine.setPos(-4, 30, 3)
pandaActor.reparentTo(npLeine)
pandaActor.setPos (8, 0, 0)
self.pandaActor = pandaActor
leineAnim = LerpHprInterval(npLeine, 20, Vec3(-360,0,0))
leineAnim.loop()
pandaActor.loop("walk")
"""
# Labyrinth einladen
maze2d = Maze2D(self)
"""
maze2d.loadFromString(
'''
*************
* * *
* *** *** **
* * **
**** * *** *
* * *
*************
'''
)
"""
maze2d.loadFromFile("level1.maz")
self.accept("escape", sys.exit)
#Set the camera position (x, y, z)
camera.setPos ( maze2d.sizeX/2.0, maze2d.sizeY/2.0, max(maze2d.sizeX, maze2d.sizeY) * 1.5 )
#Set the camera orientation (heading, pitch, roll) in degrees
camera.setHpr ( 0, -90, 0 )
"""
# Licht einschalten
# Das "DirectionalLight" funktioniert nicht;
# vermutlich weil unser Modell keine Normalenvektoren enthält.
alight = AmbientLight('alight')
alight.setColor(VBase4(0.2, 0.2, 0.2, 1))
alnp = render.attachNewNode(alight.upcastToPandaNode())
render.setLight(alnp)
dlight = DirectionalLight('dlight')
dlight.setColor(VBase4(0.8, 0.8, 0.5, 1))
#dlight.setColor(VBase4(200, 200, 1, 1))
dlnp = render.attachNewNode(dlight.upcastToPandaNode())
dlnp.setPos(0,0,0)
dlnp.setHpr(0, -40, 0)
render.setLight(dlnp)
"""
# Und ein Smiley, der per Joystick gelenkt wird
startpos = maze2d.startPos
x,y, = startpos
x += 0.5
y += 0.5
z = 0.5
self.maze = maze2d
self.avatar = Avatar(self, Point3(x,y,z))
# Der Smiley wird über Joystick 0 gelenkt
self.initJoystick({self.avatar: 0}
)
# Kann aber auch über die Tastatur gelenkt werden
self.avatar.setupKeyboardControls ("arrow_left", "arrow_right", "arrow_up", "arrow_down")
# Damit die Joystick gelesen werden, muss eine Task verwendet werden
self.controlTask = taskMgr.add(self.joystickWatcher, "joystick", priority=1)
self.controlTask.last = 0
messenger.toggleVerbose()
#end class world
#Now that our class is defined, we create an instance of it.
#Doing so calls the __init__ method set up above
w = World()
#As usual - run() must be called before anything can be shown on screen
oldout = sys.stdout
sys.stdout = open("main.log", "wt")
run()
del w
pygame.quit()
sys.stdout.close()
sys.stdout = oldout
File name: smiley.py
Content:
#!/bin/env python
# -*- coding: iso-8859-1 -*-
import direct.directbase.DirectStart #Initialize Panda and create a window
from pandac.PandaModules import * #Contains most of Panda's modules
from direct.gui.DirectGui import * #Imports Gui objects we use for putting
#text on the screen
from direct.interval.IntervalGlobal import *
from direct.showbase.DirectObject import messenger, DirectObject
from direct.task import Task
from direct.actor import Actor
import sys
import pygame
from maze2d import Maze2D
class Avatar(DirectObject):
"""
Ein vom Spieler per Joystick gelenkter Avatar
"""
def __init__(self, world, startpos, name = "Player1"):
self.collRadius = 0.5
self.speed = 4.0 # Einheiten/Sekunde
self.joystick = None
self.wantsToMoveX = 0.0
self.wantsToMoveY = 0.0
self.name = name
self.oldpos = (0, 0)
self.world = world
# Collision Detection vorbereiten
#initialize handler
self.collHandEvent=CollisionHandlerEvent()
### siehe ###-Kommentar weiter unten
### self.collHandEvent=CollisionHandlerPusher()
self.collHandEvent.addInPattern('into-%in')
self.collHandEvent.addOutPattern('outof-%in')
self.collHandEvent.addAgainPattern('again-%in')
# Und ein Smiley, der per Joystick gelenkt wird
# Für den Smiley gibt es auch Collision Detection,
# daher nehmen wir einen Knoten "smiley" mit zwei
# Unterknoten: einer CollisionSphere und einem Model
npSmiley = render.attachNewNode(name)
npSmiley.reparentTo(render)
mSmiley = loader.loadModelCopy("smiley.egg")
mSmiley.reparentTo(npSmiley)
#mSmiley.setPos (*startpos)
# Smiley so skalieren, dass er einen Durchmesser von ca. 1 hat
mSmiley.setScale (0.5)
##mSmiley.setIntoCollideMask(BitMask32.allOff())
##mSmiley.setFromCollideMask(BitMask32.allOff())
### cs = CollisionSphere(0, 0, 0, 0.499999995)
cs = CollisionSphere(0, 0, 0, self.collRadius)
cn = CollisionNode('cnodeSmiley')
cn.setFromCollideMask(BitMask32.bit(0))
cn.setIntoCollideMask(BitMask32.allOff())
cnodePath = npSmiley.attachNewNode(cn)
cnodePath.node().addSolid(cs)
cnodePath.show()
base.cTrav.addCollider(cnodePath, self.collHandEvent)
# Mit einem CollisionHandlerPusher bewegt sich
# der Smiley von selbst in Richtung y negativ.
# Wird mit einem Pusher etwa Schwerkraft auomatisch
# eingeschaltet (in y-Richtung)?
### self.collHandEvent.addCollider(cnodePath, npSmiley)
for x in range(self.world.maze.sizeX):
self.accept("into-wandX%d" % x, self.handleCollisionIn)
self.accept("again-wandX%d" % x, self.handleCollisionAgain)
for y in range(self.world.maze.sizeY):
self.accept("into-wandY%d" % y, self.handleCollisionIn)
self.accept("again-wandY%d" % y, self.handleCollisionAgain)
npSmiley.setPos (startpos)
npSmiley.setHpr (0, 0, 0)
self.npSmiley = npSmiley
self.moveTask = taskMgr.add(self.move, "move" + name, priority=2)
self.moveTask.last = 0
self.keyspressed = []
self.richtung = None
def handleCollisionIn(self, entry):
"""
TODO: Kommt nicht mit Ecken zurecht!
TODO: Die Kollision wird zu spät festgestellt,
nämlich wenn das Objekt schon eingedrungen ist.
Richtiger wäre es, wenn VOR der Bewegung
geprüft wird, ob die Bewegung zu einer Kollision
führen würde.
"""
print "into", entry
if entry.hasInto():
print " Into:", entry.getInto()
else:
print " kein Into"
if entry.hasInteriorPoint():
print " InteriorPoint:", entry.getInteriorPoint(render)
else:
print " kein InteriorPoint"
if entry.hasSurfacePoint():
print " SurfacePoint:", entry.getSurfacePoint(render)
else:
print " kein SurfacePoint"
pos = self.npSmiley.getPos()
print "pos:", pos,
normale = entry.getSurfaceNormal(render)
surfacePoint = entry.getSurfacePoint(render)
innerPoint = entry.getInteriorPoint(render)
delta = surfacePoint - innerPoint
tiefe = delta.length()
#
#if tiefe < 1e-5:
# print "Eindringtiefe minimal - ignorieren."
# return
# Wenn Kugel-Mittelpunkt auch "hinter" der Oberfläche liegt,
# dann ignoriere die Kollision
v1 = pos - surfacePoint
v2 = normale
if v1.dot(v2) < 0:
print "Kugel-Mittelpunkt liegt 'hinter' der Oberfläche - ignorieren."
return
# Wenn die Kollision entgegen der Bewegungsrichtung
# stattfindet, ignorieren
if self.richtung:
lenRichtung = self.richtung.length()
else:
lenRichtung = 0
if lenRichtung < 1e-6:
print "Richtung:", self.richtung
#print "Keine Bewegung - ignorieren"
#return
else:
# Normalisieren
richtung = self.richtung / lenRichtung
# normale sollte sowieso normalisiert sein
kreuzprod = richtung.dot(normale)
if kreuzprod > 0.0:
print "Kollision entgegen Bewegung - Ignorieren"
return
if entry.hasInto():
pos = self.npSmiley.getPos()
delta = normale * tiefe
pos += delta
print "tiefe:", tiefe, "delta:", delta, "newpos:", pos
else:
print "not hasInto"
newpos = surfacePoint + (normale * self.collRadius)
self.npSmiley.setPos(pos)
def handleCollisionOut(self, entry):
pass #print "out of", entry
def handleCollisionAgain(self, entry):
print "again",
return self.handleCollisionIn(entry)
def move(self, task):
"""
Steuerung des Spielers per Joystick
"""
#Standard technique for finding the amount of time since the last frame
dt = task.time - task.last
task.last = task.time
#If dt is large, then there has been a #hiccup that could cause the ball
#to leave the field if this functions runs, so ignore the frame
if dt > .2: return Task.cont
if "left" in self.keyspressed: self.wantsToMoveX = -dt * self.speed
if "right" in self.keyspressed: self.wantsToMoveX = +dt * self.speed
if "up" in self.keyspressed: self.wantsToMoveY = +dt * self.speed
if "down" in self.keyspressed: self.wantsToMoveY = -dt * self.speed
if self.wantsToMoveX or self.wantsToMoveY:
pos = self.npSmiley.getPos()
delta = Vec3(self.wantsToMoveX, self.wantsToMoveY, 0.0)
newpos = pos + delta
self.npSmiley.setPos(newpos)
print "move to :", newpos
self.oldpos = newpos
self.richtung = Vec3(self.wantsToMoveX, self.wantsToMoveY, 0.0)
self.wantsToMoveX = 0.0
self.wantsToMoveY = 0.0
else:
self.richtung = None
return Task.cont
def keyboardControl(self, what):
if what.endswith("-up"):
try:
self.keyspressed.remove(what[:-3])
except ValueError:
pass
else:
self.keyspressed.append(what)
def setupKeyboardControls(self, left, right, up, down):
"""
Legt fest, über welche Tastatur-Events der Avatar gesteuert werden kann.
"""
for status in ["", "-up"]:
self.accept (left+status, self.keyboardControl, ["left" + status])
self.accept (right+status, self.keyboardControl, ["right" + status])
self.accept (up+status, self.keyboardControl, ["up" + status])
self.accept (down+status, self.keyboardControl, ["down" + status])
#end class Avatar
File name: maze2d.py
Content:
#!/bin/env python
# -*- coding: iso-8859-1 -*-
from pandac.PandaModules import * #Contains most of Panda's modules
from direct.showbase.DirectObject import DirectObject
class Maze2D(DirectObject):
def __init__(self, world):
DirectObject.__init__(self)
self.strData = ""
self.sizeX = 0
self.sizeY = 0
self.unit = 5
self.points = []
self.startPos = (0,0)
self.world = world
def loadFromString(self, data):
self.strData = data
self._parseStr()
def loadFromFile(self, fname):
fp = open(fname, "rt")
try:
data = fp.read()
return self.loadFromString(data)
finally:
fp.close()
# Aufbau der Punkte:
def mazePointIdx(self, x, y, z):
"Index eines Punktes im punkteArray"
return (((z * self.sizeY1) + y) * self.sizeX1) + x
def _parseStr(self):
"""
Bildet ein Vertex-Model, basierend auf dem String.
Das Format für den String ist wie folgt:
Jede Zeile muss dieselbe Länge haben.
Jedes einzelne Zeichen steht für ein Quadrat
auf dem "Labyrinth-Spielfeld".
' ' = Freier Platz
'*' = Mauer
Beispiel:
'''
*************
* * *
* *** *** **
* * **
**** * *** *
* * *
*************
'''
"""
lines = self.strData.splitlines()
while not lines[0]:
lines.pop(0)
while not lines[-1]:
lines.pop()
# Da y von unten nach oben läuft, Reihenfolge vertauschen
lines.reverse()
self.lines = lines
self.sizeX = len(lines[0])
self.sizeX1 = self.sizeX + 1
self.sizeY = len(lines)
self.sizeY1 = self.sizeY + 1
for y, line in enumerate(lines):
if len(line) != self.sizeX:
raise ValueError ("Zeile 0 ist %d lang, Zeile %d aber %d" % (self.sizeX, y, len(line)))
# Punkte aufbauen
self.points = [None] * (self.sizeX1 * self.sizeY1 * len([0,1]))
for z in [0,1]:
for y in range(self.sizeY1):
for x in range(self.sizeX1):
self.points[self.mazePointIdx(x,y,z)] = (x,y,z)
# Punkte eintragen
bodenFormat = GeomVertexFormat.getV3()
vdata = GeomVertexData('boden', bodenFormat, Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
for point in self.points:
vertex.addData3f (*point)
# Startposition bestimmen
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == "S":
self.startPos = (x,y)
# Dreiecke aufbauen (ein Quadrat = 2 Dreiecke)
# Zuerst der "Boden" - überall da, wo kein * steht
bodenTriStrips = []
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch != '*':
# Die dazugehörigen vier Punkte bestimmen
# Wir verwenden ein TriStrip,
# daher muss das zweite Dreieck andersherum sein:
# 23
# 01
punkte = [self.mazePointIdx(*p)
for p in [(x,y,0), (x+1,y,0), (x,y+1,0), (x+1,y+1,0)]
]
bodenTriStrips.append(punkte)
# Dreiecke eintragen (Boden)
prim = GeomTristrips(Geom.UHStatic)
for ts in bodenTriStrips:
for p in ts:
prim.addVertex(p)
prim.closePrimitive()
# Und daraus einen Node machen
geom = Geom(vdata)
geom.addPrimitive(prim)
node = GeomNode('boden')
node.setIntoCollideMask(BitMask32.allOff())
node.addGeom(geom)
nodePath = render.attachNewNode(node)
nodePath.setColor(0.0, 0.1, 0.5)
# Jetzt die "Decke" - überall da, wo ein * steht
deckeTriStrips = []
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '*':
# Die dazugehörigen vier Punkte bestimmen
# Wir verwenden ein TriStrip,
# daher muss das zweite Dreieck andersherum sein:
# 23
# 01
punkte = [self.mazePointIdx(*p)
for p in [(x,y,1), (x+1,y,1), (x,y+1,1), (x+1,y+1,1)]
]
deckeTriStrips.append(punkte)
# Dreiecke eintragen (decke)
prim = GeomTristrips(Geom.UHStatic)
for ts in deckeTriStrips:
for p in ts:
prim.addVertex(p)
prim.closePrimitive()
# Und daraus einen Node machen
geom = Geom(vdata)
geom.addPrimitive(prim)
node = GeomNode('decke')
node.addGeom(geom)
nodePath = render.attachNewNode(node)
nodePath.setColor(0.4, 0.2, 0.2)
# Und jetzt noch die "Wände".
# Eine Wand muss stehen beim Ãœbergang Stern/kein Stern
# und ganz außen herum
# Zuerst die Außenwand
prim = GeomTristrips(Geom.UHStatic)
# Es gibt acht Punkte (die äußeren Ecken des Labyrinths)
punkte = []
for z in [0,1]:
for y in [0,self.sizeY]:
for x in [0,self.sizeX]:
punkte.append(self.mazePointIdx(x,y,z))
# Wir machen daraus _einen Tristrip für die Außenwand
prim.addVertex(punkte[0])
prim.addVertex(punkte[4])
prim.addVertex(punkte[1])
prim.addVertex(punkte[5])
prim.addVertex(punkte[3])
prim.addVertex(punkte[7])
prim.addVertex(punkte[2])
prim.addVertex(punkte[6])
prim.addVertex(punkte[0])
prim.addVertex(punkte[4])
prim.closePrimitive()
geom = Geom(vdata)
geom.addPrimitive(prim)
node = GeomNode('aussenwand')
node.addGeom(geom)
nodePath = render.attachNewNode(node)
nodePath.setTwoSided(True)
nodePath.setColor(0.3, 0.3, 0.3)
cnWand = CollisionNode('wand')
cnWand.setIntoCollideMask(BitMask32.bit(0))
cnpWand = render.attachNewNode(cnWand)
cnpWand.show()
# Jetzt die inneren Wände (Übergang in X-Richtung)
for x in range(self.sizeX):
if x > 0:
prim = GeomTristrips(Geom.UHStatic)
lastch = ''
for y in range(self.sizeY):
# Fallunterscheidung
if self.lines[y][x] != '*' and self.lines[y][x-1] == '*':
# Ãœbergang von Wand zu leer
punkte = [self.mazePointIdx(*p)
for p in [(x,y,0), (x,y+1,0), (x,y,1), (x,y+1,1)]
]
for p in punkte:
prim.addVertex(p)
prim.closePrimitive()
if self.lines[y][x] == '*' and self.lines[y][x-1] != '*':
# Ãœbergang von Wand zu leer
punkte = [self.mazePointIdx(*p)
for p in [(x,y,0), (x,y,1), (x,y+1,0), (x,y+1,1)]
]
for p in punkte:
prim.addVertex(p)
prim.closePrimitive()
lastch = ch
# Und daraus einen Node machen
geom = Geom(vdata)
geom.addPrimitive(prim)
node = GeomNode('wandX%d' % (x-1))
node.setIntoCollideMask(BitMask32.bit(0))
node.addGeom(geom)
nodePath = cnpWand.attachNewNode(node)
nodePath.setColor(1, 1, 0.0)
# Jetzt die inneren Wände (Übergang in Y-Richtung)
for y, line in enumerate(lines):
if y > 0:
prim = GeomTristrips(Geom.UHStatic)
for x, ch in enumerate(line):
# Fallunterscheidung
if ch != '*' and lines[y-1][x] == '*':
# Ãœbergang von Wand zu leer
punkte = [self.mazePointIdx(*p)
for p in [(x,y,0), (x,y,1), (x+1,y,0), (x+1,y,1)]
]
for p in punkte:
prim.addVertex(p)
prim.closePrimitive()
elif ch == '*' and lines[y-1][x] != '*':
# Ãœbergang von Wand zu leer
punkte = [self.mazePointIdx(*p)
for p in [(x,y,0), (x+1,y,0), (x,y,1), (x+1,y,1)]
]
for p in punkte:
prim.addVertex(p)
prim.closePrimitive()
# Und daraus einen Node machen
geom = Geom(vdata)
geom.addPrimitive(prim)
node = GeomNode('wandY%d' % (y-1))
node.setIntoCollideMask(BitMask32.bit(0))
node.addGeom(geom)
nodePath = cnpWand.attachNewNode(node)
nodePath.setColor(1, 0, 0.0)
Note:
The code contains some comments that probably don’t make sense.
In