Browse Source

*** empty log message ***

Joe Shochet 24 years ago
parent
commit
75588a1224
2 changed files with 482 additions and 0 deletions
  1. 148 0
      direct/src/distributed/ClockDelta.py
  2. 334 0
      direct/src/distributed/DistributedSmoothNode.py

+ 148 - 0
direct/src/distributed/ClockDelta.py

@@ -0,0 +1,148 @@
+# ClockDelta provides the ability to use clock synchronization for
+# distributed objects
+
+from PandaModules import *
+import DirectNotifyGlobal
+import DirectObject
+import math
+
+# The following two parameters, NetworkTimeBits and
+# NetworkTimePrecision, define the number of bits required to store a
+# network time, and the number of ticks per second it represents,
+# respectively.  The tradeoff is the longest period of elapsed time we
+# can measure, vs. the precision with which we can measure it.
+
+# 16 and 100 give us precision to 1/100th of a second, with a range of
+# +/- 5 minutes in a 16-bit integer.  These are eminently tweakable,
+# but the parameter types in toon.dc must match the number of bits
+# specified here (i.e. int16 if NetworkTimeBits is 16; int32 if
+# NetworkTimeBits is 32).
+NetworkTimeBits = 16
+NetworkTimePrecision = 100.0
+
+
+# These values are derived from the above.
+NetworkTimeMask = (1 << NetworkTimeBits) - 1
+NetworkTimeTopBits = 32 - NetworkTimeBits
+
+class ClockDelta(DirectObject.DirectObject):
+    """
+    The ClockDelta object converts between universal ("network") time,
+    which is used for all network traffic, and local time (e.g. as
+    returned by getFrameTime() or getRealTime()), which is used for
+    everything else.
+    """
+
+    notify = DirectNotifyGlobal.directNotify.newCategory('ClockDelta')
+
+    def __init__(self):
+        self.globalClock = ClockObject.getGlobalClock()
+
+        self.delta = 0
+        self.accept("resetClock", self.__resetClock)
+
+    def __resetClock(self, timeDelta):
+        """
+        this is called when the global clock gets adjusted
+        timeDelta is equal to the amount of time, in seconds,
+        that has been added to the global clock
+        """
+        self.notify.debug("adjusting timebase by %f seconds" % timeDelta)
+        # adjust our timebase by the same amount
+        self.delta += timeDelta
+
+    def resynchronize(self, localTime, networkTime):
+        """resynchronize(self, float localTime, int networkTime)
+
+        Resets the relative delta so that the indicated networkTime
+        and localTime map to the same instant.  The return value is
+        the amount by which the clock changes, in seconds.
+        """
+
+        newDelta = float(localTime) - float(networkTime) / NetworkTimePrecision
+        change = newDelta - self.delta
+        self.delta = newDelta
+
+        return self.networkToLocalTime(self.localToNetworkTime(change), 0.0)
+
+
+    ### Primary interface functions ###
+
+    def networkToLocalTime(self, networkTime, now = None):
+        """networkToLocalTime(self, int networkTime)
+
+        Converts the indicated networkTime to the corresponding
+        localTime value.  The time is assumed to be within +/- 5
+        minutes of the current local time given in now, or
+        getRealTime() if now is not specified.
+        """
+        if now == None:
+            now = self.globalClock.getRealTime()
+
+        # First, determine what network time we have for 'now'.
+        ntime = int(math.floor((now - self.delta) * NetworkTimePrecision + 0.5))
+
+        # The signed difference between these is the number of units
+        # of NetworkTimePrecision by which the network time differs
+        # from 'now'.
+        diff = self.__signExtend(networkTime - ntime)
+        
+        return now + float(diff) / NetworkTimePrecision
+
+    def localToNetworkTime(self, localTime):
+        """localToNetworkTime(self, float localTime)
+
+        Converts the indicated localTime to the corresponding
+        networkTime value.
+        """
+        ntime = int(math.floor((localTime - self.delta) * NetworkTimePrecision + 0.5))
+        return self.__signExtend(ntime)
+
+
+    ### Convenience functions ###
+
+    def getRealNetworkTime(self):
+        """getRealNetworkTime(self)
+
+        Returns the current getRealTime() expressed as a network time.
+        """
+        return self.localToNetworkTime(self.globalClock.getRealTime())
+
+    def getFrameNetworkTime(self):
+        """getFrameNetworkTime(self)
+
+        Returns the current getFrameTime() expressed as a network time.
+        """
+        return self.localToNetworkTime(self.globalClock.getFrameTime())
+
+    def localElapsedTime(self, networkTime):
+        """localElapsedTime(self, int networkTime)
+
+        Returns the amount of time elapsed (in seconds) on the client
+        since the server message was sent.  Negative values are
+        clamped to zero.
+        
+        """
+        now = self.globalClock.getFrameTime()
+        dt = now - self.networkToLocalTime(networkTime, now)
+
+        if (dt >= 0.0):
+            return dt
+        else:
+            self.notify.debug('negative clock delta: %.3f' % dt)
+            return 0.0
+
+
+
+    ### Private functions ###
+
+    def __signExtend(self, networkTime):
+        """__signExtend(self, int networkTime)
+
+        Preserves the lower NetworkTimeBits of the networkTime value,
+        and extends the sign bit all the way up.
+        """
+
+        return ((networkTime & NetworkTimeMask) << NetworkTimeTopBits) >> NetworkTimeTopBits
+
+globalClockDelta = ClockDelta()

+ 334 - 0
direct/src/distributed/DistributedSmoothNode.py

@@ -0,0 +1,334 @@
+"""DistributedSmoothNode module: contains the DistributedSmoothNode class"""
+
+from PandaModules import *
+from ClockDelta import *
+import DistributedNode
+import Task
+
+globalClock = ClockObject.getGlobalClock()
+
+# This number defines our tolerance for out-of-sync telemetry packets.
+# If a packet appears to have originated from more than MaxFuture
+# seconds in the future, assume we're out of sync with the other
+# avatar and suggest a resync for both.
+MaxFuture = base.config.GetFloat("smooth-max-future", 0.1)
+
+# These flags indicate whether global smoothing and/or prediction is
+# allowed or disallowed.
+EnableSmoothing = base.config.GetBool("smooth-enable-smoothing", 1)
+EnablePrediction = base.config.GetBool("smooth-enable-prediction", 1)
+
+# These values represent the amount of time, in seconds, to delay the
+# apparent position of other avatars, when non-predictive and
+# predictive smoothing is in effect, respectively.
+Lag = base.config.GetDouble("smooth-lag", 0.2)
+PredictionLag = base.config.GetDouble("smooth-prediction-lag", 0.0)
+
+
+
+def activateSmoothing(smoothing, prediction):
+    """
+    Enables or disables the smoothing of other avatars' motion.
+    This is a global flag that controls the behavior of all
+    SmoothMovers in the world.  If smoothing is off, no kind of
+    smoothing will be performed, regardless of the setting of
+    prediction.
+    
+    This is not necessarily predictive smoothing; if predictive
+    smoothing is off, avatars will be lagged by a certain factor
+    to achieve smooth motion.  Otherwise, if predictive smoothing
+    is on, avatars will be drawn as nearly as possible in their
+    current position, by extrapolating from old position reports.
+
+    This assumes you have a client repository that knows its
+    localToonDoId -- stored in self.cr.localToonDoId
+    """
+
+    if smoothing and EnableSmoothing:
+        if prediction and EnablePrediction:
+            # Prediction and smoothing.
+            SmoothMover.setSmoothMode(SmoothMover.SMOn)
+            SmoothMover.setPredictionMode(SmoothMover.PMOn)
+            SmoothMover.setDelay(PredictionLag)
+        else:
+            # Smoothing, but no prediction.
+            SmoothMover.setSmoothMode(SmoothMover.SMOn)
+            SmoothMover.setPredictionMode(SmoothMover.PMOff)
+            SmoothMover.setDelay(Lag)
+    else:
+        # No smoothing, no prediction.
+        SmoothMover.setSmoothMode(SmoothMover.SMOff)
+        SmoothMover.setPredictionMode(SmoothMover.PMOff)
+        SmoothMover.setDelay(0.0)
+        
+
+
+class DistributedSmoothNode(DistributedNode.DistributedNode):
+    """DistributedSmoothNode class:
+
+    This specializes DistributedNode to add functionality to smooth
+    motion over time, via the SmoothMover C++ object defined in
+    DIRECT.
+
+    """
+
+    def __init__(self, cr):
+        try:
+            self.DistributedSmoothNode_initialized
+        except:
+            self.DistributedSmoothNode_initialized = 1
+            DistributedNode.DistributedNode.__init__(self, cr)
+
+            self.smoother = SmoothMover()
+            self.smoothStarted = 0
+        return None
+
+
+    ### Methods to handle computing and updating of the smoothed
+    ### position.
+
+    def smoothPosition(self):
+        """smoothPosition(self)
+
+        This function updates the position of the node to its computed
+        smoothed position.  This may be overridden by a derived class
+        to specialize the behavior.
+
+        """
+        if self.smoother.computeSmoothPosition():
+            self.setMat(self.smoother.getSmoothMat())
+
+    def doSmoothTask(self, task):
+        self.smoothPosition()
+        return Task.cont
+
+    def startSmooth(self):
+        """startSmooth(self)
+
+        This function starts the task that ensures the node is
+        positioned correctly every frame.  However, while the task is
+        running, you won't be able to lerp the node or directly
+        position it.
+        
+        """
+        if self.isLocal():
+            # If we've just finished banging on localToon, reload the
+            # drive interface's concept of our position.
+            base.drive.node().setPos(self.getPos())
+            base.drive.node().setHpr(self.getHpr())
+
+        elif not self.smoothStarted:
+            taskName = self.taskName("smooth")
+            taskMgr.removeTasksNamed(taskName)
+            self.reloadPosition()
+            taskMgr.spawnMethodNamed(self.doSmoothTask, taskName)
+            self.smoothStarted = 1
+            
+        return
+
+    def stopSmooth(self):
+        """startSmooth(self)
+
+        This function stops the task spawned by startSmooth(), and
+        allows show code to move the node around directly.
+        """
+        if self.smoothStarted:
+            taskName = self.taskName("smooth")
+            taskMgr.removeTasksNamed(taskName)
+            self.forceToTruePosition()
+            self.smoothStarted = 0
+        return
+        
+
+
+    def forceToTruePosition(self):
+        """forceToTruePosition(self)
+
+        This forces the node to reposition itself to its latest known
+        position.  This may result in a pop as the node skips the last
+        of its lerp points.
+
+        """
+        if (not self.isLocal()) and \
+           self.smoother.getLatestPosition():
+            self.setMat(self.smoother.getSmoothMat())
+        self.smoother.clearPositions(1)
+
+    def reloadPosition(self):
+        """reloadPosition(self)
+
+        This function re-reads the position from the node itself and
+        clears any old position reports for the node.  This should be
+        used whenever show code bangs on the node position and expects
+        it to stick.
+
+        """
+        self.smoother.clearPositions(0)
+        self.smoother.setMat(self.getMat())
+        self.smoother.setTimestamp()
+        self.smoother.markPosition()
+        
+
+    
+    ### distributed set pos and hpr functions ###
+
+    ### These functions send the distributed update to set the
+    ### appropriate values on the remote side.  These are
+    ### composite fields, with all the likely combinations
+    ### defined; each function maps (via the dc file) to one or
+    ### more component operations on the remote client.
+
+    def d_setSmStop(self):
+        self.sendUpdate("setSmStop", [globalClockDelta.getFrameNetworkTime()])
+    def setSmStop(self, timestamp):
+        self.setComponentTLive(timestamp)
+
+    def d_setSmH(self, h):
+        self.sendUpdate("setSmH", [h, globalClockDelta.getFrameNetworkTime()])
+    def setSmH(self, h, timestamp):
+        self.setComponentH(h)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmXY(self, x, y):
+        self.sendUpdate("setSmXY", [x, y, globalClockDelta.getFrameNetworkTime()])
+    def setSmXY(self, x, y, timestamp):
+        self.setComponentX(x)
+        self.setComponentY(y)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmXZ(self, x, z):
+        self.sendUpdate("setSmXZ", [x, z, globalClockDelta.getFrameNetworkTime()])
+    def setSmXZ(self, x, z, timestamp):
+        self.setComponentX(x)
+        self.setComponentZ(z)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmPos(self, x, y, z):
+        self.sendUpdate("setSmPos", [x, y, z, globalClockDelta.getFrameNetworkTime()])
+    def setSmPos(self, x, y, z, timestamp):
+        self.setComponentX(x)
+        self.setComponentY(y)
+        self.setComponentZ(z)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmHpr(self, h, p, r):
+        self.sendUpdate("setSmHpr", [h, p, r, globalClockDelta.getFrameNetworkTime()])
+    def setSmHpr(self, h, p, r, timestamp):
+        self.setComponentH(h)
+        self.setComponentP(p)
+        self.setComponentR(r)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmXYH(self, x, y, h):
+        self.sendUpdate("setSmXYH", [x, y, h, globalClockDelta.getFrameNetworkTime()])
+    def setSmXYH(self, x, y, h, timestamp):
+        self.setComponentX(x)
+        self.setComponentY(y)
+        self.setComponentH(h)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmXYZH(self, x, y, z, h):
+        self.sendUpdate("setSmXYZH", [x, y, z, h, globalClockDelta.getFrameNetworkTime()])
+    def setSmXYZH(self, x, y, z, h, timestamp):
+        self.setComponentX(x)
+        self.setComponentY(y)
+        self.setComponentZ(z)
+        self.setComponentH(h)
+        self.setComponentTLive(timestamp)
+
+    def d_setSmPosHpr(self, x, y, z, h, p, r):
+        self.sendUpdate("setSmPosHpr", [x, y, z, h, p, r, globalClockDelta.getFrameNetworkTime()])
+    def setSmPosHpr(self, x, y, z, h, p, r, timestamp):
+        self.setComponentX(x)
+        self.setComponentY(y)
+        self.setComponentZ(z)
+        self.setComponentH(h)
+        self.setComponentP(p)
+        self.setComponentR(r)
+        self.setComponentTLive(timestamp)
+        return
+
+    ### component set pos and hpr functions ###
+
+    ### These are the component functions that are invoked
+    ### remotely by the above composite functions.
+
+    def setComponentX(self, x):
+        self.smoother.setX(x)
+    def setComponentY(self, y):
+        self.smoother.setY(y)
+    def setComponentZ(self, z):
+        self.smoother.setZ(z)
+    def setComponentH(self, h):
+        self.smoother.setH(h)
+    def setComponentP(self, p):
+        self.smoother.setP(p)
+    def setComponentR(self, r):
+        self.smoother.setR(r)
+    def setComponentT(self, timestamp):
+        # This is a little bit hacky.  If *this* function is called,
+        # it must have been called directly by the server, for
+        # instance to update the values previously set for some avatar
+        # that was already into the zone as we entered.  (A live
+        # update would have gone through the function called
+        # setComponentTLive, below.)
+
+        # Since we know this update came through the server, it may
+        # reflect very old data.  Thus, we can't accurately decode the
+        # network timestamp (since the network time encoding can only
+        # represent a time up to about 5 minutes in the past), but we
+        # don't really need to know the timestamp anyway.  We'll just
+        # arbitrarily place it at right now.
+        now = globalClock.getFrameTime()
+        self.smoother.setTimestamp(now)
+        self.smoother.clearPositions(1)
+        self.smoother.markPosition()
+
+    def setComponentTLive(self, timestamp):
+        # This is the variant of setComponentT() that will be called
+        # whenever we receive a live update directly from the other
+        # client.  This is because the component functions, above,
+        # call this function explicitly instead of setComponentT().
+        
+        now = globalClock.getFrameTime()
+        local = globalClockDelta.networkToLocalTime(timestamp, now)
+        chug = globalClock.getRealTime() - now
+
+        # Sanity check the timestamp from the other avatar.  It should
+        # be just slightly in the past, but it might be off by as much
+        # as this frame's amount of time forward or back.
+        howFarFuture = local - now
+        if howFarFuture - chug >= MaxFuture:
+            # Too far off; resync both of us.
+            if self.cr.timeManager != None:
+                self.cr.timeManager.synchronize("Packets from %d off by %.1f s" % (self.doId, howFarFuture))
+            self.d_suggestResync(self.cr.localToonDoId)
+        
+        self.smoother.setTimestamp(local)
+        self.smoother.markPosition()
+
+    def b_clearSmoothing(self):
+        self.d_clearSmoothing()
+        self.clearSmoothing()
+    def d_clearSmoothing(self):
+        self.sendUpdate("clearSmoothing", [0])
+    def clearSmoothing(self, bogus = None):
+        # Call this to invalidate all the old position reports
+        # (e.g. just before popping to a new position).
+        self.smoother.clearPositions(1)
+
+
+    def wrtReparentTo(self, parent):
+        # We override this NodePath method to force it to
+        # automatically reset the smoothing position when we call it.
+        if self.smoothStarted:
+            self.forceToTruePosition()
+            NodePath.wrtReparentTo(self, parent)
+            self.reloadPosition()
+        else:
+            NodePath.wrtReparentTo(self, parent)
+
+
+    def isLocal(self):
+        # Local toon will override this to return true
+        return 0