2
0

main.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. #!/usr/bin/env python
  2. # Author: Shao Zhang, Phil Saltzman
  3. # Last Updated: 2015-03-13
  4. #
  5. # This tutorial shows how to detect and respond to collisions. It uses solids
  6. # create in code and the egg files, how to set up collision masks, a traverser,
  7. # and a handler, how to detect collisions, and how to dispatch function based
  8. # on the collisions. All of this is put together to simulate a labyrinth-style
  9. # game
  10. from direct.showbase.ShowBase import ShowBase
  11. from panda3d.core import CollisionTraverser, CollisionNode
  12. from panda3d.core import CollisionHandlerQueue, CollisionRay
  13. from panda3d.core import Material, LRotationf, NodePath
  14. from panda3d.core import AmbientLight, DirectionalLight
  15. from panda3d.core import TextNode
  16. from panda3d.core import LVector3, BitMask32
  17. from direct.gui.OnscreenText import OnscreenText
  18. from direct.interval.MetaInterval import Sequence, Parallel
  19. from direct.interval.LerpInterval import LerpFunc
  20. from direct.interval.FunctionInterval import Func, Wait
  21. from direct.task.Task import Task
  22. import sys
  23. # Some constants for the program
  24. ACCEL = 70 # Acceleration in ft/sec/sec
  25. MAX_SPEED = 5 # Max speed in ft/sec
  26. MAX_SPEED_SQ = MAX_SPEED ** 2 # Squared to make it easier to use lengthSquared
  27. # Instead of length
  28. class BallInMazeDemo(ShowBase):
  29. def __init__(self):
  30. # Initialize the ShowBase class from which we inherit, which will
  31. # create a window and set up everything we need for rendering into it.
  32. ShowBase.__init__(self)
  33. # This code puts the standard title and instruction text on screen
  34. self.title = \
  35. OnscreenText(text="Panda3D: Tutorial - Collision Detection",
  36. parent=base.a2dBottomRight, align=TextNode.ARight,
  37. fg=(1, 1, 1, 1), pos=(-0.1, 0.1), scale=.08,
  38. shadow=(0, 0, 0, 0.5))
  39. self.instructions = \
  40. OnscreenText(text="Mouse pointer tilts the board",
  41. parent=base.a2dTopLeft, align=TextNode.ALeft,
  42. pos=(0.05, -0.08), fg=(1, 1, 1, 1), scale=.06,
  43. shadow=(0, 0, 0, 0.5))
  44. self.accept("escape", sys.exit) # Escape quits
  45. # Disable default mouse-based camera control. This is a method on the
  46. # ShowBase class from which we inherit.
  47. self.disableMouse()
  48. camera.setPosHpr(0, 0, 25, 0, -90, 0) # Place the camera
  49. # Load the maze and place it in the scene
  50. self.maze = loader.loadModel("models/maze")
  51. self.maze.reparentTo(render)
  52. # Most times, you want collisions to be tested against invisible geometry
  53. # rather than every polygon. This is because testing against every polygon
  54. # in the scene is usually too slow. You can have simplified or approximate
  55. # geometry for the solids and still get good results.
  56. #
  57. # Sometimes you'll want to create and position your own collision solids in
  58. # code, but it's often easier to have them built automatically. This can be
  59. # done by adding special tags into an egg file. Check maze.egg and ball.egg
  60. # and look for lines starting with <Collide>. The part is brackets tells
  61. # Panda exactly what to do. Polyset means to use the polygons in that group
  62. # as solids, while Sphere tells panda to make a collision sphere around them
  63. # Keep means to keep the polygons in the group as visable geometry (good
  64. # for the ball, not for the triggers), and descend means to make sure that
  65. # the settings are applied to any subgroups.
  66. #
  67. # Once we have the collision tags in the models, we can get to them using
  68. # NodePath's find command
  69. # Find the collision node named wall_collide
  70. self.walls = self.maze.find("**/wall_collide")
  71. # Collision objects are sorted using BitMasks. BitMasks are ordinary numbers
  72. # with extra methods for working with them as binary bits. Every collision
  73. # solid has both a from mask and an into mask. Before Panda tests two
  74. # objects, it checks to make sure that the from and into collision masks
  75. # have at least one bit in common. That way things that shouldn't interact
  76. # won't. Normal model nodes have collision masks as well. By default they
  77. # are set to bit 20. If you want to collide against actual visable polygons,
  78. # set a from collide mask to include bit 20
  79. #
  80. # For this example, we will make everything we want the ball to collide with
  81. # include bit 0
  82. self.walls.node().setIntoCollideMask(BitMask32.bit(0))
  83. # CollisionNodes are usually invisible but can be shown. Uncomment the next
  84. # line to see the collision walls
  85. #self.walls.show()
  86. # We will now find the triggers for the holes and set their masks to 0 as
  87. # well. We also set their names to make them easier to identify during
  88. # collisions
  89. self.loseTriggers = []
  90. for i in range(6):
  91. trigger = self.maze.find("**/hole_collide" + str(i))
  92. trigger.node().setIntoCollideMask(BitMask32.bit(0))
  93. trigger.node().setName("loseTrigger")
  94. self.loseTriggers.append(trigger)
  95. # Uncomment this line to see the triggers
  96. # trigger.show()
  97. # Ground_collide is a single polygon on the same plane as the ground in the
  98. # maze. We will use a ray to collide with it so that we will know exactly
  99. # what height to put the ball at every frame. Since this is not something
  100. # that we want the ball itself to collide with, it has a different
  101. # bitmask.
  102. self.mazeGround = self.maze.find("**/ground_collide")
  103. self.mazeGround.node().setIntoCollideMask(BitMask32.bit(1))
  104. # Load the ball and attach it to the scene
  105. # It is on a root dummy node so that we can rotate the ball itself without
  106. # rotating the ray that will be attached to it
  107. self.ballRoot = render.attachNewNode("ballRoot")
  108. self.ball = loader.loadModel("models/ball")
  109. self.ball.reparentTo(self.ballRoot)
  110. # Find the collison sphere for the ball which was created in the egg file
  111. # Notice that it has a from collision mask of bit 0, and an into collison
  112. # mask of no bits. This means that the ball can only cause collisions, not
  113. # be collided into
  114. self.ballSphere = self.ball.find("**/ball")
  115. self.ballSphere.node().setFromCollideMask(BitMask32.bit(0))
  116. self.ballSphere.node().setIntoCollideMask(BitMask32.allOff())
  117. # No we create a ray to start above the ball and cast down. This is to
  118. # Determine the height the ball should be at and the angle the floor is
  119. # tilting. We could have used the sphere around the ball itself, but it
  120. # would not be as reliable
  121. self.ballGroundRay = CollisionRay() # Create the ray
  122. self.ballGroundRay.setOrigin(0, 0, 10) # Set its origin
  123. self.ballGroundRay.setDirection(0, 0, -1) # And its direction
  124. # Collision solids go in CollisionNode
  125. # Create and name the node
  126. self.ballGroundCol = CollisionNode('groundRay')
  127. self.ballGroundCol.addSolid(self.ballGroundRay) # Add the ray
  128. self.ballGroundCol.setFromCollideMask(
  129. BitMask32.bit(1)) # Set its bitmasks
  130. self.ballGroundCol.setIntoCollideMask(BitMask32.allOff())
  131. # Attach the node to the ballRoot so that the ray is relative to the ball
  132. # (it will always be 10 feet over the ball and point down)
  133. self.ballGroundColNp = self.ballRoot.attachNewNode(self.ballGroundCol)
  134. # Uncomment this line to see the ray
  135. #self.ballGroundColNp.show()
  136. # Finally, we create a CollisionTraverser. CollisionTraversers are what
  137. # do the job of walking the scene graph and calculating collisions.
  138. # For a traverser to actually do collisions, you need to call
  139. # traverser.traverse() on a part of the scene. Fortunately, ShowBase
  140. # has a task that does this for the entire scene once a frame. By
  141. # assigning it to self.cTrav, we designate that this is the one that
  142. # it should call traverse() on each frame.
  143. self.cTrav = CollisionTraverser()
  144. # Collision traversers tell collision handlers about collisions, and then
  145. # the handler decides what to do with the information. We are using a
  146. # CollisionHandlerQueue, which simply creates a list of all of the
  147. # collisions in a given pass. There are more sophisticated handlers like
  148. # one that sends events and another that tries to keep collided objects
  149. # apart, but the results are often better with a simple queue
  150. self.cHandler = CollisionHandlerQueue()
  151. # Now we add the collision nodes that can create a collision to the
  152. # traverser. The traverser will compare these to all others nodes in the
  153. # scene. There is a limit of 32 CollisionNodes per traverser
  154. # We add the collider, and the handler to use as a pair
  155. self.cTrav.addCollider(self.ballSphere, self.cHandler)
  156. self.cTrav.addCollider(self.ballGroundColNp, self.cHandler)
  157. # Collision traversers have a built in tool to help visualize collisions.
  158. # Uncomment the next line to see it.
  159. #self.cTrav.showCollisions(render)
  160. # This section deals with lighting for the ball. Only the ball was lit
  161. # because the maze has static lighting pregenerated by the modeler
  162. ambientLight = AmbientLight("ambientLight")
  163. ambientLight.setColor((.55, .55, .55, 1))
  164. directionalLight = DirectionalLight("directionalLight")
  165. directionalLight.setDirection(LVector3(0, 0, -1))
  166. directionalLight.setColor((0.375, 0.375, 0.375, 1))
  167. directionalLight.setSpecularColor((1, 1, 1, 1))
  168. self.ballRoot.setLight(render.attachNewNode(ambientLight))
  169. self.ballRoot.setLight(render.attachNewNode(directionalLight))
  170. # This section deals with adding a specular highlight to the ball to make
  171. # it look shiny. Normally, this is specified in the .egg file.
  172. m = Material()
  173. m.setSpecular((1, 1, 1, 1))
  174. m.setShininess(96)
  175. self.ball.setMaterial(m, 1)
  176. # Finally, we call start for more initialization
  177. self.start()
  178. def start(self):
  179. # The maze model also has a locator in it for where to start the ball
  180. # To access it we use the find command
  181. startPos = self.maze.find("**/start").getPos()
  182. # Set the ball in the starting position
  183. self.ballRoot.setPos(startPos)
  184. self.ballV = LVector3(0, 0, 0) # Initial velocity is 0
  185. self.accelV = LVector3(0, 0, 0) # Initial acceleration is 0
  186. # Create the movement task, but first make sure it is not already
  187. # running
  188. taskMgr.remove("rollTask")
  189. self.mainLoop = taskMgr.add(self.rollTask, "rollTask")
  190. # This function handles the collision between the ray and the ground
  191. # Information about the interaction is passed in colEntry
  192. def groundCollideHandler(self, colEntry):
  193. # Set the ball to the appropriate Z value for it to be exactly on the
  194. # ground
  195. newZ = colEntry.getSurfacePoint(render).getZ()
  196. self.ballRoot.setZ(newZ + .4)
  197. # Find the acceleration direction. First the surface normal is crossed with
  198. # the up vector to get a vector perpendicular to the slope
  199. norm = colEntry.getSurfaceNormal(render)
  200. accelSide = norm.cross(LVector3.up())
  201. # Then that vector is crossed with the surface normal to get a vector that
  202. # points down the slope. By getting the acceleration in 3D like this rather
  203. # than in 2D, we reduce the amount of error per-frame, reducing jitter
  204. self.accelV = norm.cross(accelSide)
  205. # This function handles the collision between the ball and a wall
  206. def wallCollideHandler(self, colEntry):
  207. # First we calculate some numbers we need to do a reflection
  208. norm = colEntry.getSurfaceNormal(render) * -1 # The normal of the wall
  209. curSpeed = self.ballV.length() # The current speed
  210. inVec = self.ballV / curSpeed # The direction of travel
  211. velAngle = norm.dot(inVec) # Angle of incidance
  212. hitDir = colEntry.getSurfacePoint(render) - self.ballRoot.getPos()
  213. hitDir.normalize()
  214. # The angle between the ball and the normal
  215. hitAngle = norm.dot(hitDir)
  216. # Ignore the collision if the ball is either moving away from the wall
  217. # already (so that we don't accidentally send it back into the wall)
  218. # and ignore it if the collision isn't dead-on (to avoid getting caught on
  219. # corners)
  220. if velAngle > 0 and hitAngle > .995:
  221. # Standard reflection equation
  222. reflectVec = (norm * norm.dot(inVec * -1) * 2) + inVec
  223. # This makes the velocity half of what it was if the hit was dead-on
  224. # and nearly exactly what it was if this is a glancing blow
  225. self.ballV = reflectVec * (curSpeed * (((1 - velAngle) * .5) + .5))
  226. # Since we have a collision, the ball is already a little bit buried in
  227. # the wall. This calculates a vector needed to move it so that it is
  228. # exactly touching the wall
  229. disp = (colEntry.getSurfacePoint(render) -
  230. colEntry.getInteriorPoint(render))
  231. newPos = self.ballRoot.getPos() + disp
  232. self.ballRoot.setPos(newPos)
  233. # This is the task that deals with making everything interactive
  234. def rollTask(self, task):
  235. # Standard technique for finding the amount of time since the last
  236. # frame
  237. dt = base.clock.dt
  238. # If dt is large, then there has been a # hiccup that could cause the ball
  239. # to leave the field if this functions runs, so ignore the frame
  240. if dt > .2:
  241. return Task.cont
  242. # The collision handler collects the collisions. We dispatch which function
  243. # to handle the collision based on the name of what was collided into
  244. for i in range(self.cHandler.getNumEntries()):
  245. entry = self.cHandler.getEntry(i)
  246. name = entry.getIntoNode().getName()
  247. if name == "wall_collide":
  248. self.wallCollideHandler(entry)
  249. elif name == "ground_collide":
  250. self.groundCollideHandler(entry)
  251. elif name == "loseTrigger":
  252. self.loseGame(entry)
  253. # Read the mouse position and tilt the maze accordingly
  254. if base.mouseWatcherNode.hasMouse():
  255. mpos = base.mouseWatcherNode.getMouse() # get the mouse position
  256. self.maze.setP(mpos.getY() * -10)
  257. self.maze.setR(mpos.getX() * 10)
  258. # Finally, we move the ball
  259. # Update the velocity based on acceleration
  260. self.ballV += self.accelV * dt * ACCEL
  261. # Clamp the velocity to the maximum speed
  262. if self.ballV.lengthSquared() > MAX_SPEED_SQ:
  263. self.ballV.normalize()
  264. self.ballV *= MAX_SPEED
  265. # Update the position based on the velocity
  266. self.ballRoot.setPos(self.ballRoot.getPos() + (self.ballV * dt))
  267. # This block of code rotates the ball. It uses something called a quaternion
  268. # to rotate the ball around an arbitrary axis. That axis perpendicular to
  269. # the balls rotation, and the amount has to do with the size of the ball
  270. # This is multiplied on the previous rotation to incrimentally turn it.
  271. prevRot = LRotationf(self.ball.getQuat())
  272. axis = LVector3.up().cross(self.ballV)
  273. newRot = LRotationf(axis, 45.5 * dt * self.ballV.length())
  274. self.ball.setQuat(prevRot * newRot)
  275. return Task.cont # Continue the task indefinitely
  276. # If the ball hits a hole trigger, then it should fall in the hole.
  277. # This is faked rather than dealing with the actual physics of it.
  278. def loseGame(self, entry):
  279. # The triggers are set up so that the center of the ball should move to the
  280. # collision point to be in the hole
  281. toPos = entry.getInteriorPoint(render)
  282. taskMgr.remove('rollTask') # Stop the maze task
  283. # Move the ball into the hole over a short sequence of time. Then wait a
  284. # second and call start to reset the game
  285. Sequence(
  286. Parallel(
  287. LerpFunc(self.ballRoot.setX, fromData=self.ballRoot.getX(),
  288. toData=toPos.getX(), duration=.1),
  289. LerpFunc(self.ballRoot.setY, fromData=self.ballRoot.getY(),
  290. toData=toPos.getY(), duration=.1),
  291. LerpFunc(self.ballRoot.setZ, fromData=self.ballRoot.getZ(),
  292. toData=self.ballRoot.getZ() - .9, duration=.2)),
  293. Wait(1),
  294. Func(self.start)).start()
  295. # Finally, create an instance of our class and start 3d rendering
  296. demo = BallInMazeDemo()
  297. demo.run()