Browse Source

added ProjectileInterval

Darren Ranalli 22 years ago
parent
commit
613dea6208

+ 1 - 0
direct/src/interval/IntervalGlobal.py

@@ -9,5 +9,6 @@ from MopathInterval import *
 from ParticleInterval import *
 from ParticleInterval import *
 from SoundInterval import *
 from SoundInterval import *
 from WaitInterval import *
 from WaitInterval import *
+from ProjectileInterval import *
 from MetaInterval import *
 from MetaInterval import *
 from IntervalManager import *
 from IntervalManager import *

+ 216 - 0
direct/src/interval/ProjectileInterval.py

@@ -0,0 +1,216 @@
+"""ProjectileInterval module: contains the ProjectileInterval class"""
+
+from DirectObject import *
+from PandaModules import *
+from Interval import Interval
+from PythonUtil import lerp
+import PythonUtil
+
+class ProjectileInterval(Interval):
+    """ProjectileInterval class: moves a nodepath through the trajectory
+    of a projectile under the influence of gravity"""
+
+    # create ProjectileInterval DirectNotify category
+    notify = directNotify.newCategory('ProjectileInterval')
+
+    # serial num for unnamed intervals
+    projectileIntervalNum = 1
+
+    # g ~ 9.8 m/s^2 ~ 32 ft/s^2
+    gravity = 32.
+
+    # the projectile's velocity is constant in the X and Y directions.
+    # the projectile's motion in the Z (up) direction is parabolic
+    # due to the constant force of gravity, which acts in the -Z direction
+
+    def __init__(self, node, startPos = None,
+                 endPos = None, duration = None,
+                 startVel = None, endZ = None,
+                 gravityMult = None, name = None):
+        """
+        You may specify several different sets of input parameters.
+        (If startPos is not provided, it will be obtained from the node.)
+        
+        # go from startPos to endPos in duration seconds
+        startPos, endPos, duration
+        # given a starting velocity, go for a specific time period
+        startPos, startVel, duration
+        # given a starting velocity, go until you hit a given Z plane (TODO)
+        startPos, startVel, endZ
+        
+        You may alter gravity by providing a multiplier in 'gravityMult'.
+        '2.' will make gravity twice as strong, '.5' twice as weak.
+        '-1.' will reverse gravity
+        """
+        self.node = node
+
+        if name == None:
+            name = '%s-%s' % (self.__class__.__name__,
+                              self.projectileIntervalNum)
+            ProjectileInterval.projectileIntervalNum += 1
+
+            """
+            # attempt to add info about the caller
+            file, line, func = PythonUtil.callerInfo()
+            if file is not None:
+                name += '-%s:%s:%s' % (file, line, func)
+            """
+
+        args = (startPos, endPos, duration,
+                startVel, endZ, gravityMult)
+        self.needToCalcTraj = 0
+        if startPos is None:
+            assert duration is not None
+            # we can't calc the trajectory until we know our starting
+            # position; delay until the interval is actually started
+            self.trajectoryArgs = args
+            self.needToCalcTraj = 1
+        else:
+            self.__calcTrajectory(*args)
+
+        Interval.__init__(self, name, duration)
+
+    def __calcTrajectory(self, startPos = None,
+                         endPos = None, duration = None,
+                         startVel = None, endZ = None,
+                         gravityMult = None):
+        self.needToCalcTraj = 0
+        if not startPos:
+            startPos = self.node.getPos()
+
+        def doIndirections(*items):
+            result = []
+            for item in items:
+                if callable(item):
+                    item = item()
+                result.append(item)
+            return result
+
+        startPos, endPos, startVel, endZ, gravityMult = \
+                  doIndirections(startPos, endPos, startVel, endZ, gravityMult)
+
+        # we're guaranteed to know the starting position at this point
+        self.startPos = startPos
+
+        # gravity is applied in the -Z direction
+        self.zAcc = -self.gravity
+        if gravityMult:
+            self.zAcc *= gravityMult
+
+        def calcStartVel(startPos, endPos, duration, zAccel):
+            # p(t) = p_0 + t*v_0 + .5*a*t^2
+            # v_0 = [p(t) - p_0 - .5*a*t^2] / t
+            t = duration
+            return Point3((endPos[0] - startPos[0]) / duration,
+                          (endPos[1] - startPos[1]) / duration,
+                          (endPos[2] - startPos[2] - (.5*zAccel*t*t)) / t)
+
+        def calcTimeOfImpactOnPlane(startHeight, endHeight, startVel, accel):
+            return PythonUtil.solveQuadratic(accel * .5, startVel,
+                                             startHeight-endHeight)
+
+        # which set of input parameters do we have?
+        if (None not in (endPos, duration)):
+            assert not startVel
+            assert not endZ
+            self.duration = duration
+            self.endPos = endPos
+            self.startVel = calcStartVel(self.startPos, self.endPos,
+                                         self.duration, self.zAcc)
+        elif (None not in (startVel, duration)):
+            assert not endPos
+            assert not endZ
+            self.duration = duration
+            self.startVel = startVel
+            self.endPos = self.__calcPos(self.duration)
+        elif (None not in (startVel, endZ)):
+            assert not endPos
+            assert not duration
+            self.startVel = startVel
+            time = calcTimeOfImpactOnPlane(self.startPos[2], endZ,
+                                           self.startVel[2], self.zAcc)
+            if not time:
+                self.notify.error(
+                    'projectile never reaches plane Z=%s' % endZ)
+            if type(time) == type([]):
+                # projectile hits plane once going up, once going down
+                # assume they want the one on the way down
+                self.notify.debug('projectile hits plane twice at times: %s' %
+                                  time)
+                time = max(*time)
+            else:
+                self.notify.debug('projectile hits plane once at time: %s' %
+                                  time)
+            self.duration = time
+            self.endPos = self.__calcPos(self.duration)
+        else:
+            self.notify.error('invalid set of inputs')
+
+        self.notify.debug('startPos: %s' % `self.startPos`)
+        self.notify.debug('endPos:   %s' % `self.endPos`)
+        self.notify.debug('duration: %s' % self.duration)
+        self.notify.debug('startVel: %s' % `self.startVel`)
+        self.notify.debug('z-accel:  %s' % self.zAcc)
+
+    def __initialize(self):
+        if self.needToCalcTraj:
+            self.__calcTrajectory(*self.trajectoryArgs)
+
+    def privInitialize(self, t):
+        self.__initialize()
+        Interval.privInitialize(self, t)
+
+    def privInstant(self):
+        self.__initialize()
+        Interval.privInstant(self)
+
+    def __calcPos(self, t):
+        return Point3(
+            self.startPos[0] + (self.startVel[0] * t),
+            self.startPos[1] + (self.startVel[1] * t),
+            (self.startPos[2] + (self.startVel[2] * t) +
+             (.5 * self.zAcc * t * t)))
+
+    def privStep(self, t):
+        self.node.setPos(self.__calcPos(t))
+        Interval.privStep(self, t)
+
+"""
+        ##################################################################
+          TODO: support arbitrary sets of inputs
+        ##################################################################
+        You must provide a few of the parameters, and the others will be
+        computed. The input parameters in question are:
+          duration, endZ, endPos, startVel, gravityMult
+          
+        Valid sets of input parameters (AA),
+        (trivially computed/default parameters) (BB),
+        non-trivial computed parameters (CC):
+        AA && BB => CC
+        
+        # one parameter
+        duration && (startVel, gravityMult) => endZ, endPos
+        endZ     && (startVel, gravityMult) => duration, endPos
+        endPos   && (endZ, gravityMult    ) => duration, startVel
+        
+        # two parameters
+        duration, endZ        && (endPos, gravityMult) => startVel
+        duration, endPos      && (endZ, gravityMult  ) => startVel
+        duration, startVel    && (gravityMult        ) => endZ, endPos
+        duration, gravityMult && (startVel           ) => endZ, endPos
+        endZ, startVel        && (gravityMult        ) => duration, endPos
+        endZ, gravityMult     && (endPos, startVel   ) => duration
+        endPos, gravityMult   && (endZ               ) => duration, startVel
+        
+        # three parameters
+        duration, endZ, startVel        && (      ) => endPos, gravityMult
+        duration, endZ, gravityMult     && (endPos) => startVel
+        duration, endPos, gravityMult   && (endZ  ) => startVel
+        duration, startVel, gravityMult && (      ) => endZ, endPos
+        endZ, startVel, gravityMult     && (      ) => duration, endPos
+        
+        # four parameters
+        duration, endZ, startVel, gravityMult && () => endPos
+        ##################################################################
+        ##################################################################
+"""

+ 26 - 0
direct/src/showbase/PythonUtil.py

@@ -562,3 +562,29 @@ def formatElapsedSeconds(seconds):
         return "%s%d:%02d:%02d" % (sign, hours, minutes, seconds)
         return "%s%d:%02d:%02d" % (sign, hours, minutes, seconds)
     else:
     else:
         return "%s%d:%02d" % (sign, minutes, seconds)
         return "%s%d:%02d" % (sign, minutes, seconds)
+
+def solveQuadratic(a, b, c):
+    # quadratic equation: ax^2 + bx + c = 0
+    # quadratic formula:  x = [-b +/- sqrt(b^2 - 4ac)] / 2a
+    # returns None, root, or [root1, root2]
+
+    # a cannot be zero.
+    if a == 0.:
+        return None
+
+    # calculate the determinant (b^2 - 4ac)
+    D = (b * b) - (4. * a * c)
+
+    if D < 0:
+        # there are no solutions (sqrt(negative number) is undefined)
+        return None
+    elif D == 0:
+        # only one root
+        return (-b) / (2. * a)
+    else:
+        # OK, there are two roots
+        sqrtD = math.sqrt(D)
+        twoA = 2. * a
+        root1 = ((-b) - sqrtD) / twoA
+        root2 = ((-b) + sqrtD) / twoA
+        return [root1, root2]