Poodletooth-iLand/panda/direct/interval/ProjectileInterval.py
Master Jumblespeed d882959bfa switch to remote
2015-05-18 22:11:33 -04:00

301 lines
12 KiB
Python
Executable file

"""ProjectileInterval module: contains the ProjectileInterval class"""
__all__ = ['ProjectileInterval']
from pandac.PandaModules import *
from Interval import Interval
from direct.showbase.PythonUtil import lerp
from direct.showbase 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,
wayPoint = None, timeToWayPoint = None,
gravityMult = None, name = None,
collNode = None):
"""
You may specify several different sets of input parameters.
(If startPos is not provided, it will be obtained from the node's
position at the time that the interval is first started. Note that
in this case you must provide a duration of some kind.)
# 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
startPos, startVel, endZ
# pass through wayPoint at time 'timeToWayPoint'. Go until
# you hit a given Z plane
startPos, wayPoint, timeToWayPoint, endZ
You may alter gravity by providing a multiplier in 'gravityMult'.
'2.' will make gravity twice as strong, '.5' half as strong.
'-1.' will reverse gravity
If collNode is not None, it should be an empty CollisionNode
which will be filled with an appropriate CollisionParabola
when the interval starts. This CollisionParabola will be set
to match the interval's parabola, and its t1, t2 values will
be updated automatically as the interval plays. It will *not*
be automatically removed from the node when the interval
finishes.
"""
self.node = node
self.collNode = collNode
if self.collNode:
if isinstance(self.collNode, NodePath):
self.collNode = self.collNode.node()
assert self.collNode.getNumSolids() == 0
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,
wayPoint, timeToWayPoint, gravityMult)
self.implicitStartPos = 0
if startPos is None:
if duration is None:
self.notify.error('must provide either startPos or duration')
self.duration = duration
# we can't calc the trajectory until we know our starting
# position; delay until the interval is actually started
self.trajectoryArgs = args
self.implicitStartPos = 1
else:
self.trajectoryArgs = args
self.__calcTrajectory(*args)
Interval.__init__(self, name, self.duration)
def __calcTrajectory(self, startPos = None,
endPos = None, duration = None,
startVel = None, endZ = None,
wayPoint = None, timeToWayPoint = None,
gravityMult = None):
if startPos is None:
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, wayPoint, \
timeToWayPoint = \
doIndirections(startPos, endPos, startVel, endZ, gravityMult,
wayPoint, timeToWayPoint)
# 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
if (duration == 0):
return Point3(0, 0, 0)
else:
return Point3((endPos[0] - startPos[0]) / duration,
(endPos[1] - startPos[1]) / duration,
(endPos[2] - startPos[2] - (.5*zAccel*duration*duration)) / duration)
def calcTimeOfImpactOnPlane(startHeight, endHeight, startVel, accel):
return PythonUtil.solveQuadratic(accel * .5, startVel,
startHeight-endHeight)
def calcTimeOfLastImpactOnPlane(startHeight, endHeight,
startVel, accel):
time = calcTimeOfImpactOnPlane(startHeight, endHeight,
startVel, accel)
if not time:
return None
if type(time) == type([]):
# projectile hits plane once going up, once going down
# assume they want the one on the way down
assert self.notify.debug('projectile hits plane twice at times: %s' %
time)
time = max(*time)
else:
assert self.notify.debug('projectile hits plane once at time: %s' %
time)
return time
# now all we need is startVel, duration, and endPos.
# which set of input parameters do we have?
if (None not in (endPos, duration)):
assert not startVel
assert not endZ
assert not wayPoint
assert not timeToWayPoint
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
assert not wayPoint
assert not timeToWayPoint
self.duration = duration
self.startVel = startVel
self.endPos = None
elif (None not in (startVel, endZ)):
assert not endPos
assert not duration
assert not wayPoint
assert not timeToWayPoint
self.startVel = startVel
time = calcTimeOfLastImpactOnPlane(self.startPos[2], endZ,
self.startVel[2], self.zAcc)
if time is None:
self.notify.error(
'projectile never reaches plane Z=%s' % endZ)
self.duration = time
self.endPos = None
elif (None not in (wayPoint, timeToWayPoint, endZ)):
assert not endPos
assert not duration
assert not startVel
# we pass through wayPoint at time 'timeToWayPoint', and we
# stop when we reach endZ
self.startVel = calcStartVel(self.startPos, wayPoint,
timeToWayPoint, self.zAcc)
time = calcTimeOfLastImpactOnPlane(
self.startPos[2], endZ, self.startVel[2], self.zAcc)
if time is None:
self.notify.error(
'projectile never reaches plane Z=%s' % endZ)
self.duration = time
self.endPos = None
else:
self.notify.error('invalid set of inputs to ProjectileInterval')
self.parabola = LParabola(VBase3(0, 0, 0.5 * self.zAcc),
self.startVel,
self.startPos)
if not self.endPos:
self.endPos = self.__calcPos(self.duration)
# these are the parameters that we need to know:
assert self.notify.debug('startPos: %s' % repr(self.startPos))
assert self.notify.debug('endPos: %s' % repr(self.endPos))
assert self.notify.debug('duration: %s' % self.duration)
assert self.notify.debug('startVel: %s' % repr(self.startVel))
assert self.notify.debug('z-accel: %s' % self.zAcc)
def __initialize(self):
if self.implicitStartPos:
self.__calcTrajectory(*self.trajectoryArgs)
def testTrajectory(self):
try:
self.__calcTrajectory(*self.trajectoryArgs)
except StandardError:
assert self.notify.error('invalid projectile parameters')
return False
return True
def privInitialize(self, t):
self.__initialize()
if self.collNode:
self.collNode.clearSolids()
csolid = CollisionParabola(self.parabola, 0, 0)
self.collNode.addSolid(csolid)
Interval.privInitialize(self, t)
def privInstant(self):
self.__initialize()
Interval.privInstant(self)
if self.collNode:
self.collNode.clearSolids()
csolid = CollisionParabola(self.parabola, 0, self.duration)
self.collNode.addSolid(csolid)
def __calcPos(self, t):
return self.parabola.calcPoint(t)
def privStep(self, t):
self.node.setFluidPos(self.__calcPos(t))
Interval.privStep(self, t)
if self.collNode and self.collNode.getNumSolids() > 0:
csolid = self.collNode.modifySolid(0)
csolid.setT1(csolid.getT2())
csolid.setT2(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
##################################################################
##################################################################
"""