DistancePhasedNode.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. from direct.showbase.DirectObject import DirectObject
  2. from direct.directnotify.DirectNotifyGlobal import directNotify
  3. from panda3d.core import (
  4. BitMask32,
  5. CollisionHandlerEvent,
  6. CollisionNode,
  7. CollisionSphere,
  8. CollisionTraverser,
  9. NodePath,
  10. )
  11. from .PhasedObject import PhasedObject
  12. class DistancePhasedNode(PhasedObject, DirectObject, NodePath):
  13. """
  14. This class defines a PhasedObject,NodePath object that will handle
  15. the phasing of an object in the scene graph according to its
  16. distance from some other collider object(such as an avatar).
  17. Since it's a NodePath, you can parent it to another object in the
  18. scene graph, or even inherit from this class to get its functionality.
  19. What you will need to define to use this class:
  20. - The distances at which you want the phases to load/unload
  21. - Whether you want the object to clean itself up or not when
  22. exitting the largest distance sphere
  23. - What the load/unload functions are
  24. - What sort of events to listen for when a collision occurs
  25. - (Optional) A collision bitmask for the phase collision spheres
  26. - (Optional) A 'from' collision node to collide into our 'into' spheres
  27. You specify the distances and function names by the phaseParamMap
  28. parameter to `__init__()`. For example::
  29. phaseParamMap = {'Alias': distance, ...}
  30. ...
  31. def loadPhaseAlias(self):
  32. pass
  33. def unloadPhaseAlias(self):
  34. pass
  35. If the 'fromCollideNode' is supplied, we will set up our own
  36. traverser and only traverse below this node. It will send out
  37. events of the form '<enterPrefix>%in' and '<exitPrefix>%in' in
  38. order to match the main collision traverser's patterns. Note
  39. that this will only be used after a reset or phase change in
  40. order to fully transition to the correct phase in a single pass.
  41. Most of the time, it will be reacting to events from the main
  42. collision traverser.
  43. IMPORTANT:
  44. The following only applies when ``autoCleanup is True``:
  45. If you unload the last phase, by either calling `cleanup()` or
  46. by exiting the last phase's distance, you will need to
  47. explicitly call `reset()` to get the distance phasing to work
  48. again. This was done so if either this node or the collider is
  49. removed from the scene graph (e.g. avatar teleport), the phased
  50. object will clean itself up automatically.
  51. """
  52. notify = directNotify.newCategory("DistancePhasedObject")
  53. __InstanceSequence = 0
  54. __InstanceDeque = []
  55. @staticmethod
  56. def __allocateId():
  57. """
  58. Give each phase node a unique id in order to filter out
  59. collision events from other phase nodes. We do it in
  60. this manner so the client doesn't need to worry about
  61. giving each phase node a unique name.
  62. """
  63. if DistancePhasedNode.__InstanceDeque:
  64. return DistancePhasedNode.__InstanceDeque.pop(0)
  65. else:
  66. id = DistancePhasedNode.__InstanceSequence
  67. DistancePhasedNode.__InstanceSequence += 1
  68. DistancePhasedNode.__InstanceSequence &= 65535
  69. return id
  70. @staticmethod
  71. def __deallocateId(id):
  72. """
  73. Reuse abandoned ids.
  74. """
  75. DistancePhasedNode.__InstanceDeque.append(id)
  76. def __init__(self, name, phaseParamMap = {},
  77. autoCleanup = True,
  78. enterPrefix = 'enter', exitPrefix = 'exit',
  79. phaseCollideMask = BitMask32.allOn(),
  80. fromCollideNode = None):
  81. NodePath.__init__(self, name)
  82. self.phaseParamMap = phaseParamMap
  83. self.phaseParamList = sorted(list(phaseParamMap.items()),
  84. key = lambda x: x[1],
  85. reverse = True)
  86. PhasedObject.__init__(self,
  87. dict([(alias,phase) for (phase,alias) in enumerate([item[0] for item in self.phaseParamList])]))
  88. self.__id = self.__allocateId()
  89. self.autoCleanup = autoCleanup
  90. self.enterPrefix = enterPrefix
  91. self.exitPrefix = exitPrefix
  92. self.phaseCollideMask = phaseCollideMask
  93. self.cTrav = base.cTrav
  94. self.fromCollideNode = fromCollideNode
  95. self._colSpheres = []
  96. self.reset()
  97. def __del__(self):
  98. self.__deallocateId(self.__id)
  99. def __repr__(self):
  100. outStr = 'DistancePhasedObject('
  101. outStr += repr(self.getName())
  102. for param, value in zip(('phaseParamMap', 'autoCleanup', 'enterPrefix', 'exitPrefix', 'phaseCollideMask', 'fromCollideNode'),
  103. ({}, True, 'enter', 'exit', BitMask32.allOn(), None)):
  104. pv = getattr(self, param)
  105. if pv != value:
  106. outStr += ', %s = %r' % (param, pv)
  107. outStr += ')'
  108. return outStr
  109. def __str__(self):
  110. return '%s in phase \'%s\'' % (NodePath.__str__(self), self.getPhase())
  111. def cleanup(self):
  112. """
  113. Disables all collisions.
  114. Ignores all owned event listeners.
  115. Unloads all unloaded phases.
  116. """
  117. self.__disableCollisions(cleanup = True)
  118. for sphere in self._colSpheres:
  119. sphere.remove()
  120. self._colSpheres = []
  121. PhasedObject.cleanup(self)
  122. def setPhaseCollideMask(self, mask):
  123. """
  124. Sets the intoCollideMasks for our collision spheres.
  125. """
  126. self.phaseCollideMask = mask
  127. for sphere in self._colSpheres:
  128. self.colSphere.node().setIntoCollideMask(self.phaseCollideMask)
  129. def reset(self):
  130. """
  131. Unloads all loaded phases and puts the phase node
  132. in the startup state is if it had just been initialized.
  133. """
  134. self.cleanup()
  135. self.__oneTimeCollide()
  136. for name, dist in self.phaseParamList:
  137. cSphere = CollisionSphere(0.0, 0.0, 0.0, dist)
  138. cSphere.setTangible(0)
  139. cName = 'PhaseNode%s-%d' % (name, self.__id)
  140. cSphereNode = CollisionNode(cName)
  141. cSphereNode.setIntoCollideMask(self.phaseCollideMask)
  142. cSphereNode.setFromCollideMask(BitMask32.allOff())
  143. cSphereNode.addSolid(cSphere)
  144. cSphereNodePath = self.attachNewNode(cSphereNode)
  145. cSphereNodePath.stash()
  146. # cSphereNodePath.show() # For debugging
  147. self._colSpheres.append(cSphereNodePath)
  148. if self.fromCollideNode:
  149. self.cTrav = CollisionTraverser()
  150. cHandler = CollisionHandlerEvent()
  151. cHandler.addInPattern(self.enterPrefix + '%in')
  152. cHandler.addOutPattern(self.exitPrefix + '%in')
  153. self.cTrav.addCollider(self.fromCollideNode,cHandler)
  154. self.__enableCollisions(-1)
  155. def setPhase(self, aPhase):
  156. """
  157. See PhasedObject.setPhase()
  158. """
  159. phase = self.getAliasPhase(aPhase)
  160. PhasedObject.setPhase(self, aPhase)
  161. self.__disableCollisions()
  162. self.__enableCollisions(phase)
  163. if phase == -1 and self.autoCleanup:
  164. self.cleanup()
  165. else:
  166. self.__oneTimeCollide()
  167. def __getEnterEvent(self, phaseName):
  168. return '%sPhaseNode%s-%d' % (self.enterPrefix, phaseName, self.__id)
  169. def __getExitEvent(self, phaseName):
  170. return '%sPhaseNode%s-%d' % (self.exitPrefix, phaseName, self.__id)
  171. def __enableCollisions(self, phase):
  172. """
  173. Turns on collisions for the spheres bounding this
  174. phase zone by unstashing their geometry. Enables
  175. the exit event for the larger and the enter event
  176. for the smaller. Handles the extreme(end) phases
  177. gracefully.
  178. """
  179. if 0 <= phase:
  180. phaseName = self.getPhaseAlias(phase)
  181. self.accept(self.__getExitEvent(phaseName),
  182. self.__handleExitEvent,
  183. extraArgs = [phaseName])
  184. self._colSpheres[phase].unstash()
  185. if 0 <= phase+1 < len(self._colSpheres):
  186. phaseName = self.getPhaseAlias(phase+1)
  187. self.accept(self.__getEnterEvent(phaseName),
  188. self.__handleEnterEvent,
  189. extraArgs = [phaseName])
  190. self._colSpheres[phase+1].unstash()
  191. def __disableCollisions(self, cleanup = False):
  192. """
  193. Disables all collision geometry by stashing
  194. the geometry. If autoCleanup == True and we're
  195. not currently cleaning up, leave the exit event
  196. and collision sphere active for the largest(thus lowest)
  197. phase. This is so that we can still cleanup if
  198. the phase node exits the largest sphere.
  199. """
  200. for x,sphere in enumerate(self._colSpheres):
  201. phaseName = self.getPhaseAlias(x)
  202. self.ignore(self.__getEnterEvent(phaseName))
  203. if x > 0 or not self.autoCleanup or cleanup:
  204. sphere.stash()
  205. self.ignore(self.__getExitEvent(phaseName))
  206. def __handleEnterEvent(self, phaseName, cEntry):
  207. self.setPhase(phaseName)
  208. def __handleExitEvent(self, phaseName, cEntry):
  209. phase = self.getAliasPhase(phaseName) - 1
  210. self.setPhase(phase)
  211. def __oneTimeCollide(self):
  212. """
  213. Fire off a one-time collision traversal of the
  214. scene graph. This allows us to process our entire
  215. phasing process in one frame in the cases where
  216. we cross more than one phase border.
  217. """
  218. if self.cTrav:
  219. if self.cTrav is base.cTrav:
  220. # we use 'render'here since if we only try to
  221. # traverse ourself, we end up calling exit
  222. # events for the rest of the eventHandlers.
  223. # Consider supplying the fromCollideNode parameter.
  224. self.cTrav.traverse(render)
  225. else:
  226. # Only traverse ourself
  227. self.cTrav.traverse(self)
  228. base.eventMgr.doEvents()
  229. class BufferedDistancePhasedNode(DistancePhasedNode):
  230. """
  231. This class is similar to DistancePhasedNode except you can also
  232. specify a buffer distance for each phase. Upon entering that phase,
  233. its distance will be increased by the buffer amount. Conversely,
  234. the distance will be decremented by that amount, back to its
  235. original size, upon leaving. In this manner, you can avoid the problem
  236. of 'phase flicker' as someone repeatedly steps across a static phase
  237. border.
  238. You specify the buffer amount in the bufferParamMap parameter
  239. to :meth:`__init__()`. It has this format::
  240. bufferParamMap = {'alias':(distance, bufferAmount), ...}
  241. """
  242. notify = directNotify.newCategory("BufferedDistancePhasedObject")
  243. def __init__(self, name, bufferParamMap = {}, autoCleanup = True,
  244. enterPrefix = 'enter', exitPrefix = 'exit', phaseCollideMask = BitMask32.allOn(), fromCollideNode = None):
  245. self.bufferParamMap = bufferParamMap
  246. self.bufferParamList = sorted(list(bufferParamMap.items()),
  247. key = lambda x: x[1],
  248. reverse = True)
  249. sParams = dict(bufferParamMap)
  250. for key in sParams:
  251. sParams[key] = sParams[key][0]
  252. DistancePhasedNode.__init__(self, name = name,
  253. phaseParamMap = sParams,
  254. autoCleanup = autoCleanup,
  255. enterPrefix = enterPrefix,
  256. exitPrefix = exitPrefix,
  257. phaseCollideMask = phaseCollideMask,
  258. fromCollideNode = fromCollideNode)
  259. def __repr__(self):
  260. outStr = 'BufferedDistancePhasedNode('
  261. outStr += repr(self.getName())
  262. for param, value in zip(('bufferParamMap', 'autoCleanup', 'enterPrefix', 'exitPrefix', 'phaseCollideMask', 'fromCollideNode'),
  263. ({}, True, 'enter', 'exit', BitMask32.allOn(), None)):
  264. pv = getattr(self, param)
  265. if pv != value:
  266. outStr += ', %s = %r' % (param, pv)
  267. outStr += ')'
  268. return outStr
  269. def __str__(self):
  270. return '%s in phase \'%s\'' % (NodePath.__str__(self), self.getPhase())
  271. def setPhase(self, aPhase):
  272. """
  273. see DistancePhasedNode.setPhase()
  274. """
  275. DistancePhasedNode.setPhase(self, aPhase)
  276. phase = self.getAliasPhase(aPhase)
  277. self.__adjustCollisions(phase)
  278. def __adjustCollisions(self, phase):
  279. for x,sphere in enumerate(self._colSpheres[:phase+1]):
  280. sphere.node().modifySolid(0).setRadius(self.bufferParamList[x][1][1])
  281. sphere.node().markInternalBoundsStale()
  282. for x,sphere in enumerate(self._colSpheres[phase+1:]):
  283. sphere.node().modifySolid(0).setRadius(self.bufferParamList[x+phase+1][1][0])
  284. sphere.node().markInternalBoundsStale()
  285. if __debug__ and 0:
  286. cSphere = CollisionSphere(0, 0, 0, 0.1)
  287. cNode = CollisionNode('camCol')
  288. cNode.addSolid(cSphere)
  289. cNodePath = NodePath(cNode)
  290. cNodePath.reparentTo(base.cam)
  291. # cNodePath.show()
  292. # cNodePath.setPos(25,0,0)
  293. base.cTrav = CollisionTraverser()
  294. eventHandler = CollisionHandlerEvent()
  295. eventHandler.addInPattern('enter%in')
  296. eventHandler.addOutPattern('exit%in')
  297. # messenger.toggleVerbose()
  298. base.cTrav.addCollider(cNodePath, eventHandler)
  299. p = BufferedDistancePhasedNode('p', {'At': (10, 20), 'Near': (100, 200), 'Far': (1000, 1020)},
  300. autoCleanup=False,
  301. fromCollideNode=cNodePath,
  302. )
  303. p.reparentTo(render)
  304. p._DistancePhasedNode__oneTimeCollide()
  305. base.eventMgr.doEvents()