"""ProjectileInterval module: contains the ProjectileInterval class"""
__all__ = ['ProjectileInterval']
from panda3d.core import *
from panda3d.direct import *
from direct.directnotify.DirectNotifyGlobal import *
from .Interval import Interval
from direct.showbase import PythonUtil
[docs]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
[docs] 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 is None:
name = '%s-%s' % (self.__class__.__name__,
self.projectileIntervalNum)
ProjectileInterval.projectileIntervalNum += 1
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 isinstance(time, list):
# 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)
[docs] def testTrajectory(self):
try:
self.__calcTrajectory(*self.trajectoryArgs)
except Exception:
assert self.notify.error('invalid projectile parameters')
return False
return True
[docs] 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)
[docs] 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)
[docs] 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)