Wrong results with collision detection?

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