DistributedSmoothNode.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. """DistributedSmoothNode module: contains the DistributedSmoothNode class"""
  2. from pandac.PandaModules import *
  3. from ClockDelta import *
  4. import DistributedNode
  5. import DistributedSmoothNodeBase
  6. from direct.task.Task import cont
  7. # This number defines our tolerance for out-of-sync telemetry packets.
  8. # If a packet appears to have originated from more than MaxFuture
  9. # seconds in the future, assume we're out of sync with the other
  10. # avatar and suggest a resync for both.
  11. MaxFuture = base.config.GetFloat("smooth-max-future", 0.2)
  12. # How frequently can we suggest a resynchronize with another client?
  13. MinSuggestResync = base.config.GetFloat("smooth-min-suggest-resync", 15)
  14. # These flags indicate whether global smoothing and/or prediction is
  15. # allowed or disallowed.
  16. EnableSmoothing = base.config.GetBool("smooth-enable-smoothing", 1)
  17. EnablePrediction = base.config.GetBool("smooth-enable-prediction", 1)
  18. # These values represent the amount of time, in seconds, to delay the
  19. # apparent position of other avatars, when non-predictive and
  20. # predictive smoothing is in effect, respectively. This is in
  21. # addition to the automatic delay of the observed average latency from
  22. # each avatar, which is intended to compensate for relative clock
  23. # skew.
  24. Lag = base.config.GetDouble("smooth-lag", 0.2)
  25. PredictionLag = base.config.GetDouble("smooth-prediction-lag", 0.0)
  26. GlobalSmoothing = 0
  27. GlobalPrediction = 0
  28. def globalActivateSmoothing(smoothing, prediction):
  29. """ Globally activates or deactivates smoothing and prediction on
  30. all DistributedSmoothNodes currently in existence, or yet to be
  31. generated. """
  32. global GlobalSmoothing, GlobalPrediction
  33. GlobalSmoothing = smoothing
  34. GlobalPrediction = prediction
  35. for obj in base.cr.getAllOfType(DistributedSmoothNode):
  36. obj.activateSmoothing(smoothing, prediction)
  37. # For historical reasons, we temporarily define
  38. # DistributedSmoothNode.activateSmoothing() to be the global function.
  39. # We'll remove this soon, so it won't get confused with the instance
  40. # method, below.
  41. activateSmoothing = globalActivateSmoothing
  42. class DistributedSmoothNode(DistributedNode.DistributedNode,
  43. DistributedSmoothNodeBase.DistributedSmoothNodeBase):
  44. """
  45. This specializes DistributedNode to add functionality to smooth
  46. motion over time, via the SmoothMover C++ object defined in
  47. DIRECT.
  48. """
  49. def __init__(self, cr):
  50. try:
  51. self.DistributedSmoothNode_initialized
  52. except:
  53. self.DistributedSmoothNode_initialized = 1
  54. DistributedNode.DistributedNode.__init__(self, cr)
  55. DistributedSmoothNodeBase.DistributedSmoothNodeBase.__init__(self)
  56. self.cnode.setRepository(cr, 0, 0)
  57. self.smoother = SmoothMover()
  58. self.smoothStarted = 0
  59. self.lastSuggestResync = 0
  60. self._smoothWrtReparents = False
  61. def delete(self):
  62. DistributedSmoothNodeBase.DistributedSmoothNodeBase.delete(self)
  63. DistributedNode.DistributedNode.delete(self)
  64. def generate(self):
  65. DistributedNode.DistributedNode.generate(self)
  66. self.activateSmoothing(GlobalSmoothing, GlobalPrediction)
  67. ### Methods to handle computing and updating of the smoothed
  68. ### position.
  69. def smoothPosition(self):
  70. """
  71. This function updates the position of the node to its computed
  72. smoothed position. This may be overridden by a derived class
  73. to specialize the behavior.
  74. """
  75. self.smoother.computeAndApplySmoothMat(self)
  76. def doSmoothTask(self, task):
  77. self.smoothPosition()
  78. return cont
  79. def wantsSmoothing(self):
  80. # Override this function to return 0 if this particular kind
  81. # of smooth node doesn't really want to be smoothed.
  82. return 1
  83. def startSmooth(self):
  84. """
  85. This function starts the task that ensures the node is
  86. positioned correctly every frame. However, while the task is
  87. running, you won't be able to lerp the node or directly
  88. position it.
  89. """
  90. if not self.wantsSmoothing() or self.isDisabled() or self.isLocal():
  91. return
  92. if not self.smoothStarted:
  93. taskName = self.taskName("smooth")
  94. taskMgr.remove(taskName)
  95. self.reloadPosition()
  96. taskMgr.add(self.doSmoothTask, taskName)
  97. self.smoothStarted = 1
  98. def stopSmooth(self):
  99. """
  100. This function stops the task spawned by startSmooth(), and
  101. allows show code to move the node around directly.
  102. """
  103. if self.smoothStarted:
  104. taskName = self.taskName("smooth")
  105. taskMgr.remove(taskName)
  106. self.forceToTruePosition()
  107. self.smoothStarted = 0
  108. def setSmoothWrtReparents(self, flag):
  109. self._smoothWrtReparents = flag
  110. def getSmoothWrtReparents(self):
  111. return self._smoothWrtReparents
  112. def forceToTruePosition(self):
  113. """
  114. This forces the node to reposition itself to its latest known
  115. position. This may result in a pop as the node skips the last
  116. of its lerp points.
  117. """
  118. #printStack()
  119. if (not self.isLocal()) and \
  120. self.smoother.getLatestPosition():
  121. self.smoother.applySmoothMat(self)
  122. self.smoother.clearPositions(1)
  123. def reloadPosition(self):
  124. """
  125. This function re-reads the position from the node itself and
  126. clears any old position reports for the node. This should be
  127. used whenever show code bangs on the node position and expects
  128. it to stick.
  129. """
  130. #printStack()
  131. self.smoother.clearPositions(0)
  132. self.smoother.setMat(self.getMat())
  133. self.smoother.setPhonyTimestamp()
  134. self.smoother.markPosition()
  135. # distributed set pos and hpr functions
  136. # 'send' versions are inherited from DistributedSmoothNodeBase
  137. def setSmStop(self, timestamp=None):
  138. self.setComponentTLive(timestamp)
  139. def setSmH(self, h, timestamp=None):
  140. self.setComponentH(h)
  141. self.setComponentTLive(timestamp)
  142. def setSmZ(self, z, timestamp=None):
  143. self.setComponentZ(z)
  144. self.setComponentTLive(timestamp)
  145. def setSmXY(self, x, y, timestamp=None):
  146. self.setComponentX(x)
  147. self.setComponentY(y)
  148. self.setComponentTLive(timestamp)
  149. def setSmXZ(self, x, z, timestamp=None):
  150. self.setComponentX(x)
  151. self.setComponentZ(z)
  152. self.setComponentTLive(timestamp)
  153. def setSmPos(self, x, y, z, timestamp=None):
  154. self.setComponentX(x)
  155. self.setComponentY(y)
  156. self.setComponentZ(z)
  157. self.setComponentTLive(timestamp)
  158. def setSmHpr(self, h, p, r, timestamp=None):
  159. self.setComponentH(h)
  160. self.setComponentP(p)
  161. self.setComponentR(r)
  162. self.setComponentTLive(timestamp)
  163. def setSmXYH(self, x, y, h, timestamp):
  164. self.setComponentX(x)
  165. self.setComponentY(y)
  166. self.setComponentH(h)
  167. self.setComponentTLive(timestamp)
  168. def setSmXYZH(self, x, y, z, h, timestamp=None):
  169. self.setComponentX(x)
  170. self.setComponentY(y)
  171. self.setComponentZ(z)
  172. self.setComponentH(h)
  173. self.setComponentTLive(timestamp)
  174. def setSmPosHpr(self, x, y, z, h, p, r, timestamp=None):
  175. self.setComponentX(x)
  176. self.setComponentY(y)
  177. self.setComponentZ(z)
  178. self.setComponentH(h)
  179. self.setComponentP(p)
  180. self.setComponentR(r)
  181. self.setComponentTLive(timestamp)
  182. ### component set pos and hpr functions ###
  183. ### These are the component functions that are invoked
  184. ### remotely by the above composite functions.
  185. def setComponentX(self, x):
  186. self.smoother.setX(x)
  187. def setComponentY(self, y):
  188. self.smoother.setY(y)
  189. def setComponentZ(self, z):
  190. self.smoother.setZ(z)
  191. def setComponentH(self, h):
  192. self.smoother.setH(h)
  193. def setComponentP(self, p):
  194. self.smoother.setP(p)
  195. def setComponentR(self, r):
  196. self.smoother.setR(r)
  197. def setComponentT(self, timestamp):
  198. # This is a little bit hacky. If *this* function is called,
  199. # it must have been called directly by the server, for
  200. # instance to update the values previously set for some avatar
  201. # that was already into the zone as we entered. (A live
  202. # update would have gone through the function called
  203. # setComponentTLive, below.)
  204. # Since we know this update came through the server, it may
  205. # reflect very old data. Thus, we can't accurately decode the
  206. # network timestamp (since the network time encoding can only
  207. # represent a time up to about 5 minutes in the past), but we
  208. # don't really need to know the timestamp anyway. We'll just
  209. # arbitrarily place it at right now.
  210. self.smoother.setPhonyTimestamp()
  211. self.smoother.clearPositions(1)
  212. self.smoother.markPosition()
  213. self.forceToTruePosition()
  214. def setComponentTLive(self, timestamp):
  215. # This is the variant of setComponentT() that will be called
  216. # whenever we receive a live update directly from the other
  217. # client. This is because the component functions, above,
  218. # call this function explicitly instead of setComponentT().
  219. #print 'setComponentTLive: %s' % timestamp
  220. if timestamp is None:
  221. # if no timestamp, re-use the most recent timestamp to keep things
  222. # from getting out of order
  223. if self.smoother.hasMostRecentTimestamp():
  224. self.smoother.setTimestamp(self.smoother.getMostRecentTimestamp())
  225. else:
  226. # no most-recent timestamp, use current time
  227. self.smoother.setPhonyTimestamp()
  228. self.smoother.markPosition()
  229. else:
  230. now = globalClock.getFrameTime()
  231. local = globalClockDelta.networkToLocalTime(timestamp, now)
  232. realTime = globalClock.getRealTime()
  233. chug = realTime - now
  234. # Sanity check the timestamp from the other avatar. It should
  235. # be just slightly in the past, but it might be off by as much
  236. # as this frame's amount of time forward or back.
  237. howFarFuture = local - now
  238. if howFarFuture - chug >= MaxFuture:
  239. # Too far off; advise the other client of our clock information.
  240. if globalClockDelta.getUncertainty() != None and \
  241. realTime - self.lastSuggestResync >= MinSuggestResync:
  242. self.lastSuggestResync = realTime
  243. timestampB = globalClockDelta.localToNetworkTime(realTime)
  244. serverTime = realTime - globalClockDelta.getDelta()
  245. self.notify.info(
  246. "Suggesting resync for %s, with discrepency %s; local time is %s and server time is %s." % (
  247. self.doId, howFarFuture - chug,
  248. realTime, serverTime))
  249. self.d_suggestResync(
  250. self.cr.localAvatarDoId, timestamp,
  251. timestampB, serverTime,
  252. globalClockDelta.getUncertainty())
  253. self.smoother.setTimestamp(local)
  254. self.smoother.markPosition()
  255. def clearSmoothing(self, bogus = None):
  256. # Call this to invalidate all the old position reports
  257. # (e.g. just before popping to a new position).
  258. #printStack()
  259. self.smoother.clearPositions(1)
  260. def wrtReparentTo(self, parent):
  261. # We override this NodePath method to force it to
  262. # automatically reset the smoothing position when we call it.
  263. if self.smoothStarted:
  264. if self._smoothWrtReparents:
  265. #print self.getParent(), parent, self.getParent().getPos(parent)
  266. self.smoother.handleWrtReparent(self.getParent(), parent)
  267. NodePath.wrtReparentTo(self, parent)
  268. else:
  269. self.forceToTruePosition()
  270. NodePath.wrtReparentTo(self, parent)
  271. self.reloadPosition()
  272. else:
  273. NodePath.wrtReparentTo(self, parent)
  274. def d_setParent(self, parentToken):
  275. # We override this DistributedNode method to force a full position
  276. # update immediately after the distributed setParent is sent.
  277. # See ParentMgr.py for an explanation.
  278. DistributedNode.DistributedNode.d_setParent(self, parentToken)
  279. self.forceToTruePosition()
  280. self.sendCurrentPosition()
  281. ### Monitor clock sync ###
  282. def d_suggestResync(self, avId, timestampA, timestampB,
  283. serverTime, uncertainty):
  284. serverTimeSec = math.floor(serverTime)
  285. serverTimeUSec = (serverTime - serverTimeSec) * 10000.0
  286. self.sendUpdate("suggestResync", [avId, timestampA, timestampB,
  287. serverTimeSec, serverTimeUSec,
  288. uncertainty])
  289. def suggestResync(self, avId, timestampA, timestampB,
  290. serverTimeSec, serverTimeUSec, uncertainty):
  291. """
  292. This message is sent from one client to another when the other
  293. client receives a timestamp from this client that is so far
  294. out of date as to suggest that one or both clients needs to
  295. resynchronize their clock information.
  296. """
  297. serverTime = float(serverTimeSec) + float(serverTimeUSec) / 10000.0
  298. result = self.peerToPeerResync(
  299. avId, timestampA, serverTime, uncertainty)
  300. if result >= 0 and \
  301. globalClockDelta.getUncertainty() != None:
  302. other = self.cr.doId2do.get(avId)
  303. if (not other):
  304. self.notify.info(
  305. "Warning: couldn't find the avatar %d" % (avId))
  306. elif hasattr(other, "d_returnResync"):
  307. realTime = globalClock.getRealTime()
  308. serverTime = realTime - globalClockDelta.getDelta()
  309. self.notify.info(
  310. "Returning resync for %s; local time is %s and server time is %s." % (
  311. self.doId, realTime, serverTime))
  312. other.d_returnResync(
  313. self.cr.localAvatarDoId, timestampB,
  314. serverTime,
  315. globalClockDelta.getUncertainty())
  316. def d_returnResync(self, avId, timestampB, serverTime, uncertainty):
  317. serverTimeSec = math.floor(serverTime)
  318. serverTimeUSec = (serverTime - serverTimeSec) * 10000.0
  319. self.sendUpdate("returnResync", [
  320. avId, timestampB, serverTimeSec, serverTimeUSec, uncertainty])
  321. def returnResync(self, avId, timestampB, serverTimeSec, serverTimeUSec,
  322. uncertainty):
  323. """
  324. A reply sent by a client whom we recently sent suggestResync
  325. to, this reports the client's new delta information so we can
  326. adjust our clock as well.
  327. """
  328. serverTime = float(serverTimeSec) + float(serverTimeUSec) / 10000.0
  329. self.peerToPeerResync(avId, timestampB, serverTime, uncertainty)
  330. def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty):
  331. gotSync = globalClockDelta.peerToPeerResync(
  332. avId, timestamp, serverTime, uncertainty)
  333. # If we didn't get anything useful from the other client,
  334. # maybe our clock is just completely hosed. Go ask the AI.
  335. if not gotSync:
  336. if self.cr.timeManager != None:
  337. self.cr.timeManager.synchronize("suggested by %d" % (avId))
  338. return gotSync
  339. def activateSmoothing(self, smoothing, prediction):
  340. """
  341. Enables or disables the smoothing of other avatars' motion.
  342. This used to be a global flag, but now it is specific to each
  343. avatar instance. However, see globalActivateSmoothing() in
  344. this module.
  345. If smoothing is off, no kind of smoothing will be performed,
  346. regardless of the setting of prediction.
  347. This is not necessarily predictive smoothing; if predictive
  348. smoothing is off, avatars will be lagged by a certain factor
  349. to achieve smooth motion. Otherwise, if predictive smoothing
  350. is on, avatars will be drawn as nearly as possible in their
  351. current position, by extrapolating from old position reports.
  352. This assumes you have a client repository that knows its
  353. localAvatarDoId -- stored in self.cr.localAvatarDoId
  354. """
  355. if smoothing and EnableSmoothing:
  356. if prediction and EnablePrediction:
  357. # Prediction and smoothing.
  358. self.smoother.setSmoothMode(SmoothMover.SMOn)
  359. self.smoother.setPredictionMode(SmoothMover.PMOn)
  360. self.smoother.setDelay(PredictionLag)
  361. else:
  362. # Smoothing, but no prediction.
  363. self.smoother.setSmoothMode(SmoothMover.SMOn)
  364. self.smoother.setPredictionMode(SmoothMover.PMOff)
  365. self.smoother.setDelay(Lag)
  366. else:
  367. # No smoothing, no prediction.
  368. self.smoother.setSmoothMode(SmoothMover.SMOff)
  369. self.smoother.setPredictionMode(SmoothMover.PMOff)
  370. self.smoother.setDelay(0.0)