Просмотр исходного кода

made automatic cleanup optional. Now can have only one sphere if desired as long as you explicitly call cleanup when done with it

Josh Wilson 19 лет назад
Родитель
Сommit
50cc26d546
2 измененных файлов с 110 добавлено и 34 удалено
  1. 92 31
      direct/src/showbase/DistancePhasedNode.py
  2. 18 3
      direct/src/showbase/PhasedObject.py

+ 92 - 31
direct/src/showbase/DistancePhasedNode.py

@@ -14,9 +14,11 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
 
     What you will need to define to use this class:
      - The distances at which you want the phases to load/unload
+     - Whether you want the object to clean itself up or not when
+       exitting the largest distance sphere
      - What the load/unload functions are
-     - What sort of events to listen for to signal a collision
-     - (Optional) - a collision bitmask for the phase collision spheres.
+     - What sort of events to listen for when a collision occurs
+     - (Optional) - a collision bitmask for the phase collision spheres
 
     You specify the distances and function names by the phaseParamMap
     parameter to __init__().  For example:
@@ -28,7 +30,8 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
     def unloadPhaseAlias(self):
         pass
 
-    IMPORTANT!: If you unload the last phase, by either calling
+    IMPORTANT!: The following only applies when autoCleanup == True:
+                If you unload the last phase, by either calling
                 cleanup() or by exitting the last phase's distance,
                 you will need to explicitly call reset() to get the
                 distance phasing to work again. This was done so if
@@ -43,6 +46,12 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
         
     @staticmethod
     def __allocateId():
+        """
+        Give each phase node a unique id in order to filter out
+        collision events from other phase nodes.  We do it in
+        this manner so the client doesn't need to worry about
+        giving each phase node a unique name.
+        """
         if DistancePhasedNode.__InstanceDeque:
             return DistancePhasedNode.__InstanceDeque.pop(0)
         else:
@@ -53,10 +62,15 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
 
     @staticmethod
     def __deallocateId(id):
+        """
+        Reuse abandoned ids.
+        """
         DistancePhasedNode.__InstanceDeque.append(id)
 
     def __init__(self, name, phaseParamMap = {},
-                 enterPrefix = 'enter', exitPrefix = 'exit', phaseCollideMask = BitMask32.allOn()):
+                 autoCleanup = True,
+                 enterPrefix = 'enter', exitPrefix = 'exit',
+                 phaseCollideMask = BitMask32.allOn()):
         NodePath.__init__(self, name)
         self.phaseParamMap = phaseParamMap
         self.phaseParamList = sorted(phaseParamMap.items(),
@@ -66,7 +80,7 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
                               dict([(alias,phase) for (phase,alias) in enumerate([item[0] for item in self.phaseParamList])]))
         self.__id = self.__allocateId()
 
-        
+        self.autoCleanup = autoCleanup
         self.phaseCollideMask = phaseCollideMask
         self.enterPrefix = enterPrefix
         self.exitPrefix = exitPrefix
@@ -77,6 +91,19 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
     def __del__(self):
         self.__deallocateId(self.__id)
 
+    def __repr__(self):
+        outStr = 'DistancePhasedObject('
+        outStr += '%s' % `self.getName()`
+        for param, value in zip(('phaseParamMap', 'autoCleanup', 'enterPrefix', 'exitPrefix', 'phaseCollideMask'),
+                                ('{}', 'True','\'enter\'','\'exit\'','BitMask32.allOn()')):
+            outStr += eval('(\', ' + param + ' = %s\' % `self.' + param + '`,\'\')[self.' + param + ' == ' + value + ']')
+        outStr += ')'
+        return outStr
+
+    def __str__(self):
+        return '%s in phase \'%s\'' % (NodePath.__str__(self), self.getPhase())
+        
+
     def cleanup(self):
         """
         Disables all collisions.
@@ -107,16 +134,17 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
         for name, dist in self.phaseParamList:
             cSphere = CollisionSphere(0.0, 0.0, 0.0, dist)
             cSphere.setTangible(0)
-            cName = 'Phase%s-%d' % (name, self.__id)
+            cName = 'PhaseNode%s-%d' % (name, self.__id)
             cSphereNode = CollisionNode(cName)
             cSphereNode.setIntoCollideMask(self.phaseCollideMask)
             cSphereNode.setFromCollideMask(BitMask32.allOff())
             cSphereNode.addSolid(cSphere)
             cSphereNodePath = self.attachNewNode(cSphereNode)
             cSphereNodePath.stash()
+            cSphereNodePath.show()
             self._colSpheres.append(cSphereNodePath)
 
-        self.__enableCollisions(-1, startup = True)
+        self.__enableCollisions(-1)
         
     def setPhase(self, aPhase):
         """
@@ -124,47 +152,55 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
         """
         phase = self.getAliasPhase(aPhase)
         PhasedObject.setPhase(self, aPhase)
-        self.__disableCollisions(cleanup = (phase == -1))
+        self.__disableCollisions()
         self.__enableCollisions(phase)
         
-        if phase == -1:
+        if phase == -1 and self.autoCleanup:
             self.cleanup()
         else:
             self.__oneTimeCollide()
         
     def __getEnterEvent(self, phaseName):
-        return '%sPhase%s-%d' % (self.enterPrefix, phaseName, self.__id)
+        return '%sPhaseNode%s-%d' % (self.enterPrefix, phaseName, self.__id)
 
     def __getExitEvent(self, phaseName):
-        return '%sPhase%s-%d' % (self.exitPrefix, phaseName, self.__id)
+        return '%sPhaseNode%s-%d' % (self.exitPrefix, phaseName, self.__id)
     
-    def __enableCollisions(self, phase, startup = False):
-        if startup:
-            phaseName = self.getPhaseAlias(0)
-            self.accept(self.__getExitEvent(phaseName),
-                        self.__handleExitEvent,
-                        extraArgs = [phaseName])
-            self._colSpheres[0].unstash()
-            
+    def __enableCollisions(self, phase):
+        """
+        Turns on collisions for the spheres bounding this
+        phase zone by unstashing their geometry.  Enables
+        the exit event for the larger and the enter event
+        for the smaller.  Handles the  extreme(end) phases
+        gracefully.
+        """
         if 0 <= phase:
             phaseName = self.getPhaseAlias(phase)
             self.accept(self.__getExitEvent(phaseName),
                         self.__handleExitEvent,
                         extraArgs = [phaseName])
             self._colSpheres[phase].unstash()
-            
-        if 0 <= phase < len(self._colSpheres)-1 or startup:
-            phaseName = self.getPhaseAlias(phase + 1)
+
+        if 0 <= phase+1 < len(self._colSpheres):
+            phaseName = self.getPhaseAlias(phase+1)
             self.accept(self.__getEnterEvent(phaseName),
                         self.__handleEnterEvent,
                         extraArgs = [phaseName])
             self._colSpheres[phase+1].unstash()
 
     def __disableCollisions(self, cleanup = False):
+        """
+        Disables all collision geometry by stashing
+        the geometry.  If autoCleanup == True and we're
+        not currently cleaning up, leave the exit event
+        and collision sphere active for the largest(thus lowest)
+        phase.  This is so that we can still cleanup if
+        the phase node exits the largest sphere.
+        """
         for x,sphere in enumerate(self._colSpheres):
             phaseName = self.getPhaseAlias(x)
             self.ignore(self.__getEnterEvent(phaseName))
-            if x > 0 or cleanup:
+            if x > 0 or not self.autoCleanup or cleanup:
                 sphere.stash()
                 self.ignore(self.__getExitEvent(phaseName))
         
@@ -176,7 +212,13 @@ class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
         self.setPhase(phase)
 
     def __oneTimeCollide(self):
-        # we use render here since if we only try to
+        """
+        Fire off a one-time collision traversal of the
+        scene graph.  This allows us to process our entire
+        phasing process in one frame in the cases where
+        we cross more than one phase border.
+        """
+        # we use 'render'here since if we only try to
         # traverse ourself, we end up calling exit
         # events for the rest of the eventHandlers. >:(
         base.cTrav.traverse(render)
@@ -186,9 +228,9 @@ class BufferedDistancePhasedNode(DistancePhasedNode):
     """
     This class is similar to DistancePhasedNode except you can also
     specify a buffer distance for each phase.  Upon entering that phase,
-    its distance will be increased by the buffer amount.  Likewise,
-    upon leaving the distance will be decremented by that amount, back
-    to it's original size.  In this manner, you can avoid the problem
+    its distance will be increased by the buffer amount.  Conversely,
+    the distance will be decremented by that amount, back to its
+    original size, upon leaving.  In this manner, you can avoid the problem
     of 'phase flicker' as someone repeatedly steps across a static phase
     border.
 
@@ -199,17 +241,34 @@ class BufferedDistancePhasedNode(DistancePhasedNode):
     """
     notify = directNotify.newCategory("BufferedDistancePhasedObject")
 
-    def __init__(self, name, bufferParamMap = {},
+    def __init__(self, name, bufferParamMap = {}, autoCleanup = True,
                  enterPrefix = 'enter', exitPrefix = 'exit', phaseCollideMask = BitMask32.allOn()):
         sParams = dict(bufferParamMap)
         for key in sParams:
             sParams[key] = sParams[key][0]
-        DistancePhasedNode.__init__(self, name, sParams, enterPrefix, exitPrefix, phaseCollideMask)
+        DistancePhasedNode.__init__(self, name = name,
+                                    phaseParamMap = sParams,
+                                    autoCleanup = autoCleanup,
+                                    enterPrefix = enterPrefix,
+                                    exitPrefix = exitPrefix,
+                                    phaseCollideMask = phaseCollideMask)
         self.bufferParamMap = bufferParamMap
         self.bufferParamList = sorted(bufferParamMap.items(),
                                       key = lambda x: x[1],
                                       reverse = True)
 
+    def __repr__(self):
+        outStr = 'BufferedDistancePhasedNode('
+        outStr += '%s' % `self.getName()`
+        for param, value in zip(('bufferParamMap', 'autoCleanup', 'enterPrefix', 'exitPrefix', 'phaseCollideMask'),
+                                ('{}', 'True','\'enter\'','\'exit\'','BitMask32.allOn()')):
+            outStr += eval('(\', ' + param + ' = %s\' % `self.' + param + '`,\'\')[self.' + param + ' == ' + value + ']')
+        outStr += ')'
+        return outStr
+
+    def __str__(self):
+        return '%s in phase \'%s\'' % (NodePath.__str__(self), self.getPhase())
+
     def setPhase(self, aPhase):
         """
         see DistancePhasedNode.setPhase()
@@ -226,7 +285,8 @@ class BufferedDistancePhasedNode(DistancePhasedNode):
         for x,sphere in enumerate(self._colSpheres[phase+1:]):
             sphere.node().getSolid(0).setRadius(self.bufferParamList[x+phase+1][1][0])
             sphere.node().markInternalBoundsStale()
-            
+
+    
 if __debug__ and 0:
     cSphere = CollisionSphere(0,0,0,0.1)
     cNode = CollisionNode('camCol')
@@ -245,7 +305,8 @@ if __debug__ and 0:
     # messenger.toggleVerbose()
     base.cTrav.addCollider(cNodePath,eventHandler)
 
-    p = BufferedDistancePhasedNode('p',{'At':(10,20),'Near':(100,200),'Far':(1000, 1020)})
+    p = BufferedDistancePhasedNode('p',{'At':(10,20),'Near':(100,200),'Far':(1000, 1020)},
+                                   autoCleanup = True)
 
     p.reparentTo(render)
     p._DistancePhasedNode__oneTimeCollide()

+ 18 - 3
direct/src/showbase/PhasedObject.py

@@ -1,4 +1,4 @@
-from direct.directnotify import DirectNotifyGlobal
+from direct.directnotify.DirectNotifyGlobal import *
 
 class PhasedObject:
     """
@@ -35,9 +35,18 @@ class PhasedObject:
         self.phase = -1
         self.phaseAliasMap = {}
         self.aliasPhaseMap = {}
+        self.__phasing = False
 
         for alias,phase in aliasMap.items():
             self.setAlias(phase, alias)
+
+    def __repr__(self):
+        return 'PhasedObject(%s)' % str(self.aliasPhaseMap)
+
+    def __str__(self):
+        outStr = PhasedObject.__repr__(self)
+        outStr += ' in phase \'%s\'' % self.getPhase()
+        return outStr
         
     def setAlias(self, phase, alias):
         """
@@ -83,9 +92,13 @@ class PhasedObject:
         functions corresponding to the difference between the current
         phase and aPhase, starting at the current phase.
         """
+        assert not self.__phasing, 'Already phasing. Cannot setPhase() while phasing in progress.'
+        self.__phasing = True
+        
         phase = self.aliasPhaseMap.get(aPhase,aPhase)
         assert isinstance(phase,int), 'Phase alias \'%s\' not found' % aPhase
         assert phase >= -1, 'Invalid phase number \'%s\'' % phase
+        
         if phase > self.phase:
             for x in range(self.phase + 1, phase + 1):
                 self.__loadPhase(x)
@@ -93,6 +106,8 @@ class PhasedObject:
             for x in range(self.phase, phase, -1):
                 self.__unloadPhase(x)
 
+        self.__phasing = False
+
     def cleanup(self):
         """
         Will force the unloading, in correct order, of all currently
@@ -102,16 +117,16 @@ class PhasedObject:
             self.setPhase(-1)
 
     def __loadPhase(self, phase):
-        self.phase = phase
         aPhase = self.phaseAliasMap.get(phase,phase)
         getattr(self, 'loadPhase%s' % aPhase,
                 lambda: self.__phaseNotFound('load',aPhase))()
+        self.phase = phase
 
     def __unloadPhase(self, phase):
-        self.phase = (phase - 1)
         aPhase = self.phaseAliasMap.get(phase,phase)
         getattr(self, 'unloadPhase%s' % aPhase,
                 lambda: self.__phaseNotFound('unload',aPhase))()
+        self.phase = (phase - 1)
 
     def __phaseNotFound(self, mode, aPhase):
         assert self.notify.debug('%s%s() not found!\n' % (mode,aPhase))