Browse Source

"adding PhasedObjects to showbase"

Josh Wilson 18 years ago
parent
commit
0698befa25
2 changed files with 430 additions and 0 deletions
  1. 251 0
      direct/src/showbase/DistancePhasedNode.py
  2. 179 0
      direct/src/showbase/PhasedObject.py

+ 251 - 0
direct/src/showbase/DistancePhasedNode.py

@@ -0,0 +1,251 @@
+from direct.showbase.DirectObject import DirectObject
+from direct.directnotify import DirectNotifyGlobal
+from pandac.PandaModules import *
+from PhasedObject import PhasedObject
+
+class DistancePhasedNode(NodePath, DirectObject, PhasedObject):
+    """
+    This class defines a PhasedObject,NodePath object that will handle the phasing
+    of an object in the scene graph according to its distance from some 
+    other collider object(such as an avatar).
+
+    Since it's a NodePath, you can parent it to another object in the
+    scene graph, or even inherit from this class to get its functionality.
+
+    What you will need to define to use this class:
+     - The distances at which you want the phases to load/unload
+     - 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.
+
+    You specify the distances and function names by the phaseParamMap
+    parameter to __init__().  For example:
+
+    phaseParamMap = {'Alias': distance, ...}
+    ...
+    def loadPhaseAlias(self):
+        pass
+    def unloadPhaseAlias(self):
+        pass
+
+    IMPORTANT!: 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
+                either this node or the collider is removed from the
+                scene graph(eg. avatar teleport), the phased object
+                will clean itself up automatically.
+    """
+
+    notify = directNotify.newCategory("DistancePhasedObject")
+    __InstanceSequence = 0
+    __InstanceDeque = []
+        
+    @staticmethod
+    def __allocateId():
+        if DistancePhasedNode.__InstanceDeque:
+            return DistancePhasedNode.__InstanceDeque.pop(0)
+        else:
+            id = DistancePhasedNode.__InstanceSequence
+            DistancePhasedNode.__InstanceSequence += 1
+            DistancePhasedNode.__InstanceSequence &= 65535
+            return id
+
+    @staticmethod
+    def __deallocateId(id):
+        DistancePhasedNode.__InstanceDeque.append(id)
+
+    def __init__(self, name, phaseParamMap = {},
+                 enterPrefix = 'enter', exitPrefix = 'exit', phaseCollideMask = BitMask32.allOn()):
+        NodePath.__init__(self, name)
+        self.phaseParamMap = phaseParamMap
+        self.phaseParamList = sorted(phaseParamMap.items(),
+                                     key = lambda x: x[1],
+                                     reverse = True)
+        PhasedObject.__init__(self,
+                              dict([(alias,phase) for (phase,alias) in enumerate([item[0] for item in self.phaseParamList])]))
+        self.__id = self.__allocateId()
+
+        
+        self.phaseCollideMask = phaseCollideMask
+        self.enterPrefix = enterPrefix
+        self.exitPrefix = exitPrefix
+        self._colSpheres = []
+        
+        self.reset()
+
+    def __del__(self):
+        self.__deallocateId(self.__id)
+
+    def cleanup(self):
+        """
+        Disables all collisions.
+        Ignores all owned event listeners.
+        Unloads all unloaded phases.        
+        """
+        self.__disableCollisions(cleanup = True)
+        for sphere in self._colSpheres:
+            sphere.remove()
+        self._colSpheres = []
+        PhasedObject.cleanup(self)
+        
+    def setPhaseCollideMask(self, mask):
+        """
+        Sets the intoCollideMasks for our collision spheres.
+        """
+        self.phaseCollideMask = mask
+        for sphere in self._colSpheres:
+            self.colSphere.node().setIntoCollideMask(self.phaseCollideMask)
+            
+    def reset(self):
+        """
+        Unloads all loaded phases and puts the phase node
+        in the startup state is if it had just been initialized.
+        """
+        self.cleanup()
+        self.__oneTimeCollide()        
+        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)
+            cSphereNode = CollisionNode(cName)
+            cSphereNode.setIntoCollideMask(self.phaseCollideMask)
+            cSphereNode.addSolid(cSphere)
+            cSphereNodePath = self.attachNewNode(cSphereNode)
+            cSphereNodePath.show()
+            cSphereNodePath.stash()
+            self._colSpheres.append(cSphereNodePath)
+
+        self.__enableCollisions(-1, startup = True)
+        
+    def setPhase(self, aPhase):
+        """
+        See PhasedObject.setPhase()
+        """
+        phase = self.getAliasPhase(aPhase)
+        PhasedObject.setPhase(self, aPhase)
+        self.__disableCollisions(cleanup = (phase == -1))
+        self.__enableCollisions(phase)
+        
+        if phase == -1:
+            self.cleanup()
+        else:
+            self.__oneTimeCollide()
+        
+    def __getEnterEvent(self, phaseName):
+        return '%sPhase%s-%d' % (self.enterPrefix, phaseName, self.__id)
+
+    def __getExitEvent(self, phaseName):
+        return '%sPhase%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()
+            
+        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)
+            self.accept(self.__getEnterEvent(phaseName),
+                        self.__handleEnterEvent,
+                        extraArgs = [phaseName])
+            self._colSpheres[phase+1].unstash()
+
+    def __disableCollisions(self, cleanup = False):
+        for x,sphere in enumerate(self._colSpheres):
+            phaseName = self.getPhaseAlias(x)
+            self.ignore(self.__getEnterEvent(phaseName))
+            if x > 0 or cleanup:
+                sphere.stash()
+                self.ignore(self.__getExitEvent(phaseName))
+        
+    def __handleEnterEvent(self, phaseName, cEntry):
+        print cEntry
+        self.setPhase(phaseName)
+
+    def __handleExitEvent(self, phaseName, cEntry):
+        print cEntry
+        phase = self.getAliasPhase(phaseName) - 1
+        self.setPhase(phase)
+
+    def __oneTimeCollide(self):
+        base.cTrav.traverse(self)
+        base.eventMgr.doEvents()
+        
+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
+    of 'phase flicker' as someone repeatedly steps across a static phase
+    border.
+
+    You specify the buffer amount in the bufferParamMap parameter
+    to __init__().  It has this format:
+
+    bufferParamMap = {'alias':(distance, bufferAmount), ...}
+    """
+    notify = directNotify.newCategory("BufferedDistancePhasedObject")
+
+    def __init__(self, name, bufferParamMap = {}):
+        sParams = dict(bufferParamMap)
+        for key in sParams:
+            sParams[key] = sParams[key][0]
+        DistancePhasedNode.__init__(self, name, sParams)
+        self.bufferParamMap = bufferParamMap
+        self.bufferParamList = sorted(bufferParamMap.items(),
+                                      key = lambda x: x[1],
+                                      reverse = True)
+
+    def setPhase(self, aPhase):
+        """
+        see DistancePhasedNode.setPhase()
+        """
+        DistancePhasedNode.setPhase(self, aPhase)
+        phase = self.getAliasPhase(aPhase)
+        self.__adjustCollisions(phase)
+
+    def __adjustCollisions(self, phase):
+        for x,sphere in enumerate(self._colSpheres[:phase+1]):
+            sphere.node().getSolid(0).setRadius(self.bufferParamList[x][1][1])
+            sphere.node().markInternalBoundsStale()
+
+        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')
+    cNode.addSolid(cSphere)
+    cNodePath = NodePath(cNode)
+    cNodePath.reparentTo(base.cam)
+    #cNodePath.show()
+    #cNodePath.setPos(25,0,0)
+    
+    base.cTrav = CollisionTraverser()
+    
+    eventHandler = CollisionHandlerEvent()
+    eventHandler.addInPattern('enter%in')
+    eventHandler.addOutPattern('exit%in')
+    
+    # messenger.toggleVerbose()
+    base.cTrav.addCollider(cNodePath,eventHandler)
+
+    p = BufferedDistancePhasedNode('p',{'At':(10,20),'Near':(100,200),'Far':(1000, 1020)})
+
+    p.reparentTo(render)
+    p._DistancePhasedNode__oneTimeCollide()
+    base.eventMgr.doEvents()
+

+ 179 - 0
direct/src/showbase/PhasedObject.py

@@ -0,0 +1,179 @@
+from direct.directnotify import DirectNotifyGlobal
+
+class PhasedObject:
+    """
+    This class is governs the loading and unloading of successive
+    phases in an ordered and automatic manner.
+
+    An object can only have one phase at any given moment. At the
+    completion of setPhase() the current and all previous phases are
+    guaranteed to be loaded, while all later phases are guaranteed
+    to be unloaded.
+
+    In order to define a phase, simply define the functions:
+    loadPhase<#> and unloadPhase<#> where # corresponds to the number
+    of the phase to be defined and # >= 0.
+
+    You also have the ability to define alias for phases so that
+    your function definitions are more descriptive.  The way to do
+    this is to provide an aliasMap to __init__().  The aliasMap is
+    of the form {'alias':#, ...}. You can then call setPhase() with
+    this alias as well.
+
+    So for example, if you wanted to alias phase 0 to 'Far' you
+    would define loadPhaseFar() and unloadPhaseFar(). Upon calling
+    setPhase(0), setPhase('Far'), setPhase(<any phase greater than 0>),
+    or setPhase(<any alias greater than 'Far'>), loadPhaseFar() will
+    be invoked.
+
+    For a skeleton example class, see the AnfaPhasedObject class
+    definition lower in this file.
+    """
+    notify = directNotify.newCategory("PhasedObject")
+    
+    def __init__(self, aliasMap = {}):
+        self.phase = -1
+        self.phaseAliasMap = {}
+        self.aliasPhaseMap = {}
+
+        for alias,phase in aliasMap.items():
+            self.setAlias(phase, alias)
+        
+    def setAlias(self, phase, alias):
+        """
+        Map an alias to a phase number.
+
+        phase must be >= 0 and alias must be a string
+        of characters suitable for python variable names.
+        
+        The mapping must be one-to-one.        
+        """
+        assert isinstance(phase,int) and phase >= 0
+        assert isinstance(alias,str)
+        
+        self.phaseAliasMap[phase] = alias
+        self.aliasPhaseMap[alias] = phase
+
+    def getPhaseAlias(self, phase):
+        """
+        Returns the alias of a phase number, if it exists.
+        Otherwise, returns the phase number.
+        """
+        return self.phaseAliasMap.get(phase, phase)
+    
+    def getAliasPhase(self, alias):
+        """
+        Returns the phase number of an alias, if it exists.
+        Otherwise, returns the alias.
+        """
+        return self.aliasPhaseMap.get(alias, alias)
+        
+    def getPhase(self):
+        """
+        Returns the current phase (or alias, if defined)
+        this object is currently in.
+        """
+        return self.getPhaseAlias(self.phase)
+
+    def setPhase(self, aPhase):
+        """
+        aPhase can be either a phase number or a predefined alias.
+
+        Will invoke a sequence of loadPhase*() or unloadPhase*()
+        functions corresponding to the difference between the current
+        phase and aPhase, starting at the current phase.
+        """
+        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)
+        elif phase < self.phase:
+            for x in range(self.phase, phase, -1):
+                self.__unloadPhase(x)
+
+    def cleanup(self):
+        """
+        Will force the unloading, in correct order, of all currently
+        loaded phases.
+        """
+        print 'PO - cleanup'
+        if self.phase >= 0:
+            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))()
+
+    def __unloadPhase(self, phase):
+        self.phase = (phase - 1)
+        aPhase = self.phaseAliasMap.get(phase,phase)
+        getattr(self, 'unloadPhase%s' % aPhase,
+                lambda: self.__phaseNotFound('unload',aPhase))()
+
+    def __phaseNotFound(self, mode, aPhase):
+        assert self.notify.warning('skipping phase %s%s\n' % (mode,aPhase))
+        
+if __debug__:
+    class AnfaPhasedObject(PhasedObject):
+        """
+        This is an example class to demonstrate the concept of
+        alias mapping for PhasedObjects.
+
+        As the distance between an observer and this object closes,
+        we would set the phase level succesively higher, with an initial
+        phase of 'Away' being set in __init__:
+
+        setPhase('Far') -> invokes loadPhaseFar()
+        setPhase('Near') -> invokes loadPhaseNear()
+
+        Now let's say the objects start moving away from each other:
+
+        setPhase('Far') -> invokes unloadPhaseNear()
+        setPhase('Away') -> invokes unloadPhaseFar()
+
+        Now one object teleports to the other:
+
+        setPhase('At') -> invokes loadPhase('Far'),
+                          then    loadPhase('Near'),
+                          then    loadPhase('At')
+
+        Now the phased object is destroyed, we must clean it up
+        before removal:
+
+        cleanup() -> invokes unloadPhase('At')
+                     then    unloadPhase('Near')
+                     then    unloadPhase('Far')
+                     then    unloadPhase('Away')
+        """
+        def __init__(self):
+            PhasedObject.__init__(self, {'At':3, 'Near':2, 'Far':1, 'Away':0})
+            self.setPhase('Away')
+            
+        def loadPhaseAway(self):
+            print 'loading Away'
+
+        def unloadPhaseAway(self):
+            print 'unloading Away'
+                                
+        def loadPhaseFar(self):
+            print 'loading Far'
+
+        def unloadPhaseFar(self):
+            print 'unloading Far'
+            
+        def loadPhaseNear(self):
+            print 'loading Near'
+        
+        def unloadPhaseNear(self):
+            print 'unloading Near'
+        
+        def loadPhaseAt(self):
+            print 'loading At'
+        
+        def unloadPhaseAt(self):
+            print 'unloading At'
+