main.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #!/usr/bin/env python
  2. # Author: Shao Zhang, Phil Saltzman, and Greg Lindley
  3. # Last Updated: 2015-03-13
  4. #
  5. # This tutorial demonstrates the use of tasks. A task is a function that
  6. # gets called once every frame. They are good for things that need to be
  7. # updated very often. In the case of asteroids, we use tasks to update
  8. # the positions of all the objects, and to check if the bullets or the
  9. # ship have hit the asteroids.
  10. #
  11. # Note: This definitely a complicated example. Tasks are the cores of
  12. # most games so it seemed appropriate to show what a full game in Panda
  13. # could look like.
  14. from direct.showbase.ShowBase import ShowBase
  15. from panda3d.core import TextNode, TransparencyAttrib
  16. from panda3d.core import LPoint3, LVector3
  17. from direct.gui.OnscreenText import OnscreenText
  18. from direct.task.Task import Task
  19. from math import sin, cos, pi
  20. from random import randint, choice, random
  21. from direct.interval.MetaInterval import Sequence
  22. from direct.interval.FunctionInterval import Wait, Func
  23. import sys
  24. # Constants that will control the behavior of the game. It is good to
  25. # group constants like this so that they can be changed once without
  26. # having to find everywhere they are used in code
  27. SPRITE_POS = 55 # At default field of view and a depth of 55, the screen
  28. # dimensions is 40x30 units
  29. SCREEN_X = 20 # Screen goes from -20 to 20 on X
  30. SCREEN_Y = 15 # Screen goes from -15 to 15 on Y
  31. TURN_RATE = 360 # Degrees ship can turn in 1 second
  32. ACCELERATION = 10 # Ship acceleration in units/sec/sec
  33. MAX_VEL = 6 # Maximum ship velocity in units/sec
  34. MAX_VEL_SQ = MAX_VEL ** 2 # Square of the ship velocity
  35. DEG_TO_RAD = pi / 180 # translates degrees to radians for sin and cos
  36. BULLET_LIFE = 2 # How long bullets stay on screen before removed
  37. BULLET_REPEAT = .2 # How often bullets can be fired
  38. BULLET_SPEED = 10 # Speed bullets move
  39. AST_INIT_VEL = 1 # Velocity of the largest asteroids
  40. AST_INIT_SCALE = 3 # Initial asteroid scale
  41. AST_VEL_SCALE = 2.2 # How much asteroid speed multiplies when broken up
  42. AST_SIZE_SCALE = .6 # How much asteroid scale changes when broken up
  43. AST_MIN_SCALE = 1.1 # If and asteroid is smaller than this and is hit,
  44. # it disapears instead of splitting up
  45. # This helps reduce the amount of code used by loading objects, since all of
  46. # the objects are pretty much the same.
  47. def loadObject(tex=None, pos=LPoint3(0, 0), depth=SPRITE_POS, scale=1,
  48. transparency=True):
  49. # Every object uses the plane model and is parented to the camera
  50. # so that it faces the screen.
  51. obj = loader.loadModel("models/plane")
  52. obj.reparentTo(camera)
  53. # Set the initial position and scale.
  54. obj.setPos(pos.getX(), depth, pos.getY())
  55. obj.setScale(scale)
  56. # This tells Panda not to worry about the order that things are drawn in
  57. # (ie. disable Z-testing). This prevents an effect known as Z-fighting.
  58. obj.setBin("unsorted", 0)
  59. obj.setDepthTest(False)
  60. if transparency:
  61. # Enable transparency blending.
  62. obj.setTransparency(TransparencyAttrib.MAlpha)
  63. if tex:
  64. # Load and set the requested texture.
  65. tex = loader.loadTexture("textures/" + tex)
  66. obj.setTexture(tex, 1)
  67. return obj
  68. # Macro-like function used to reduce the amount to code needed to create the
  69. # on screen instructions
  70. def genLabelText(text, i):
  71. return OnscreenText(text=text, parent=base.a2dTopLeft, pos=(0.07, -.06 * i - 0.1),
  72. fg=(1, 1, 1, 1), align=TextNode.ALeft, shadow=(0, 0, 0, 0.5), scale=.05)
  73. class AsteroidsDemo(ShowBase):
  74. def __init__(self):
  75. # Initialize the ShowBase class from which we inherit, which will
  76. # create a window and set up everything we need for rendering into it.
  77. ShowBase.__init__(self)
  78. # This code puts the standard title and instruction text on screen
  79. self.title = OnscreenText(text="Panda3D: Tutorial - Tasks",
  80. parent=base.a2dBottomRight, scale=.07,
  81. align=TextNode.ARight, pos=(-0.1, 0.1),
  82. fg=(1, 1, 1, 1), shadow=(0, 0, 0, 0.5))
  83. self.escapeText = genLabelText("ESC: Quit", 0)
  84. self.leftkeyText = genLabelText("[Left Arrow]: Turn Left (CCW)", 1)
  85. self.rightkeyText = genLabelText("[Right Arrow]: Turn Right (CW)", 2)
  86. self.upkeyText = genLabelText("[Up Arrow]: Accelerate", 3)
  87. self.spacekeyText = genLabelText("[Space Bar]: Fire", 4)
  88. # Disable default mouse-based camera control. This is a method on the
  89. # ShowBase class from which we inherit.
  90. self.disableMouse()
  91. # Load the background starfield.
  92. self.setBackgroundColor((0, 0, 0, 1))
  93. self.bg = loadObject("stars.jpg", scale=146, depth=200,
  94. transparency=False)
  95. # Load the ship and set its initial velocity.
  96. self.ship = loadObject("ship.png")
  97. self.setVelocity(self.ship, LVector3.zero())
  98. # A dictionary of what keys are currently being pressed
  99. # The key events update this list, and our task will query it as input
  100. self.keys = {"turnLeft": 0, "turnRight": 0,
  101. "accel": 0, "fire": 0}
  102. self.accept("escape", sys.exit) # Escape quits
  103. # Other keys events set the appropriate value in our key dictionary
  104. self.accept("arrow_left", self.setKey, ["turnLeft", 1])
  105. self.accept("arrow_left-up", self.setKey, ["turnLeft", 0])
  106. self.accept("arrow_right", self.setKey, ["turnRight", 1])
  107. self.accept("arrow_right-up", self.setKey, ["turnRight", 0])
  108. self.accept("arrow_up", self.setKey, ["accel", 1])
  109. self.accept("arrow_up-up", self.setKey, ["accel", 0])
  110. self.accept("space", self.setKey, ["fire", 1])
  111. # Now we create the task. taskMgr is the task manager that actually
  112. # calls the function each frame. The add method creates a new task.
  113. # The first argument is the function to be called, and the second
  114. # argument is the name for the task. It returns a task object which
  115. # is passed to the function each frame.
  116. self.gameTask = taskMgr.add(self.gameLoop, "gameLoop")
  117. # Stores the time at which the next bullet may be fired.
  118. self.nextBullet = 0.0
  119. # This list will stored fired bullets.
  120. self.bullets = []
  121. # Complete initialization by spawning the asteroids.
  122. self.spawnAsteroids()
  123. # As described earlier, this simply sets a key in the self.keys dictionary
  124. # to the given value.
  125. def setKey(self, key, val):
  126. self.keys[key] = val
  127. def setVelocity(self, obj, val):
  128. obj.setPythonTag("velocity", val)
  129. def getVelocity(self, obj):
  130. return obj.getPythonTag("velocity")
  131. def setExpires(self, obj, val):
  132. obj.setPythonTag("expires", val)
  133. def getExpires(self, obj):
  134. return obj.getPythonTag("expires")
  135. def spawnAsteroids(self):
  136. # Control variable for if the ship is alive
  137. self.alive = True
  138. self.asteroids = [] # List that will contain our asteroids
  139. for i in range(10):
  140. # This loads an asteroid. The texture chosen is random
  141. # from "asteroid1.png" to "asteroid3.png".
  142. asteroid = loadObject("asteroid%d.png" % (randint(1, 3)),
  143. scale=AST_INIT_SCALE)
  144. self.asteroids.append(asteroid)
  145. # This is kind of a hack, but it keeps the asteroids from spawning
  146. # near the player. It creates the list (-20, -19 ... -5, 5, 6, 7,
  147. # ... 20) and chooses a value from it. Since the player starts at 0
  148. # and this list doesn't contain anything from -4 to 4, it won't be
  149. # close to the player.
  150. asteroid.setX(choice(tuple(range(-SCREEN_X, -5)) + tuple(range(5, SCREEN_X))))
  151. # Same thing for Y
  152. asteroid.setZ(choice(tuple(range(-SCREEN_Y, -5)) + tuple(range(5, SCREEN_Y))))
  153. # Heading is a random angle in radians
  154. heading = random() * 2 * pi
  155. # Converts the heading to a vector and multiplies it by speed to
  156. # get a velocity vector
  157. v = LVector3(sin(heading), 0, cos(heading)) * AST_INIT_VEL
  158. self.setVelocity(self.asteroids[i], v)
  159. # This is our main task function, which does all of the per-frame
  160. # processing. It takes in self like all functions in a class, and task,
  161. # the task object returned by taskMgr.
  162. def gameLoop(self, task):
  163. # Get the time elapsed since the next frame. We need this for our
  164. # distance and velocity calculations.
  165. dt = globalClock.getDt()
  166. # If the ship is not alive, do nothing. Tasks return Task.cont to
  167. # signify that the task should continue running. If Task.done were
  168. # returned instead, the task would be removed and would no longer be
  169. # called every frame.
  170. if not self.alive:
  171. return Task.cont
  172. # update ship position
  173. self.updateShip(dt)
  174. # check to see if the ship can fire
  175. if self.keys["fire"] and task.time > self.nextBullet:
  176. self.fire(task.time) # If so, call the fire function
  177. # And disable firing for a bit
  178. self.nextBullet = task.time + BULLET_REPEAT
  179. # Remove the fire flag until the next spacebar press
  180. self.keys["fire"] = 0
  181. # update asteroids
  182. for obj in self.asteroids:
  183. self.updatePos(obj, dt)
  184. # update bullets
  185. newBulletArray = []
  186. for obj in self.bullets:
  187. self.updatePos(obj, dt) # Update the bullet
  188. # Bullets have an experation time (see definition of fire)
  189. # If a bullet has not expired, add it to the new bullet list so
  190. # that it will continue to exist.
  191. if self.getExpires(obj) > task.time:
  192. newBulletArray.append(obj)
  193. else:
  194. obj.removeNode() # Otherwise, remove it from the scene.
  195. # Set the bullet array to be the newly updated array
  196. self.bullets = newBulletArray
  197. # Check bullet collision with asteroids
  198. # In short, it checks every bullet against every asteroid. This is
  199. # quite slow. A big optimization would be to sort the objects left to
  200. # right and check only if they overlap. Framerate can go way down if
  201. # there are many bullets on screen, but for the most part it's okay.
  202. for bullet in self.bullets:
  203. # This range statement makes it step though the asteroid list
  204. # backwards. This is because if an asteroid is removed, the
  205. # elements after it will change position in the list. If you go
  206. # backwards, the length stays constant.
  207. for i in range(len(self.asteroids) - 1, -1, -1):
  208. asteroid = self.asteroids[i]
  209. # Panda's collision detection is more complicated than we need
  210. # here. This is the basic sphere collision check. If the
  211. # distance between the object centers is less than sum of the
  212. # radii of the two objects, then we have a collision. We use
  213. # lengthSquared() since it is faster than length().
  214. if ((bullet.getPos() - asteroid.getPos()).lengthSquared() <
  215. (((bullet.getScale().getX() + asteroid.getScale().getX())
  216. * .5) ** 2)):
  217. # Schedule the bullet for removal
  218. self.setExpires(bullet, 0)
  219. self.asteroidHit(i) # Handle the hit
  220. # Now we do the same collision pass for the ship
  221. shipSize = self.ship.getScale().getX()
  222. for ast in self.asteroids:
  223. # Same sphere collision check for the ship vs. the asteroid
  224. if ((self.ship.getPos() - ast.getPos()).lengthSquared() <
  225. (((shipSize + ast.getScale().getX()) * .5) ** 2)):
  226. # If there is a hit, clear the screen and schedule a restart
  227. self.alive = False # Ship is no longer alive
  228. # Remove every object in asteroids and bullets from the scene
  229. for i in self.asteroids + self.bullets:
  230. i.removeNode()
  231. self.bullets = [] # Clear the bullet list
  232. self.ship.hide() # Hide the ship
  233. # Reset the velocity
  234. self.setVelocity(self.ship, LVector3(0, 0, 0))
  235. Sequence(Wait(2), # Wait 2 seconds
  236. Func(self.ship.setR, 0), # Reset heading
  237. Func(self.ship.setX, 0), # Reset position X
  238. # Reset position Y (Z for Panda)
  239. Func(self.ship.setZ, 0),
  240. Func(self.ship.show), # Show the ship
  241. Func(self.spawnAsteroids)).start() # Remake asteroids
  242. return Task.cont
  243. # If the player has successfully destroyed all asteroids, respawn them
  244. if len(self.asteroids) == 0:
  245. self.spawnAsteroids()
  246. return Task.cont # Since every return is Task.cont, the task will
  247. # continue indefinitely
  248. # Updates the positions of objects
  249. def updatePos(self, obj, dt):
  250. vel = self.getVelocity(obj)
  251. newPos = obj.getPos() + (vel * dt)
  252. # Check if the object is out of bounds. If so, wrap it
  253. radius = .5 * obj.getScale().getX()
  254. if newPos.getX() - radius > SCREEN_X:
  255. newPos.setX(-SCREEN_X)
  256. elif newPos.getX() + radius < -SCREEN_X:
  257. newPos.setX(SCREEN_X)
  258. if newPos.getZ() - radius > SCREEN_Y:
  259. newPos.setZ(-SCREEN_Y)
  260. elif newPos.getZ() + radius < -SCREEN_Y:
  261. newPos.setZ(SCREEN_Y)
  262. obj.setPos(newPos)
  263. # The handler when an asteroid is hit by a bullet
  264. def asteroidHit(self, index):
  265. # If the asteroid is small it is simply removed
  266. if self.asteroids[index].getScale().getX() <= AST_MIN_SCALE:
  267. self.asteroids[index].removeNode()
  268. # Remove the asteroid from the list of asteroids.
  269. del self.asteroids[index]
  270. else:
  271. # If it is big enough, divide it up into little asteroids.
  272. # First we update the current asteroid.
  273. asteroid = self.asteroids[index]
  274. newScale = asteroid.getScale().getX() * AST_SIZE_SCALE
  275. asteroid.setScale(newScale) # Rescale it
  276. # The new direction is chosen as perpendicular to the old direction
  277. # This is determined using the cross product, which returns a
  278. # vector perpendicular to the two input vectors. By crossing
  279. # velocity with a vector that goes into the screen, we get a vector
  280. # that is orthagonal to the original velocity in the screen plane.
  281. vel = self.getVelocity(asteroid)
  282. speed = vel.length() * AST_VEL_SCALE
  283. vel.normalize()
  284. vel = LVector3(0, 1, 0).cross(vel)
  285. vel *= speed
  286. self.setVelocity(asteroid, vel)
  287. # Now we create a new asteroid identical to the current one
  288. newAst = loadObject(scale=newScale)
  289. self.setVelocity(newAst, vel * -1)
  290. newAst.setPos(asteroid.getPos())
  291. newAst.setTexture(asteroid.getTexture(), 1)
  292. self.asteroids.append(newAst)
  293. # This updates the ship's position. This is similar to the general update
  294. # but takes into account turn and thrust
  295. def updateShip(self, dt):
  296. heading = self.ship.getR() # Heading is the roll value for this model
  297. # Change heading if left or right is being pressed
  298. if self.keys["turnRight"]:
  299. heading += dt * TURN_RATE
  300. self.ship.setR(heading % 360)
  301. elif self.keys["turnLeft"]:
  302. heading -= dt * TURN_RATE
  303. self.ship.setR(heading % 360)
  304. # Thrust causes acceleration in the direction the ship is currently
  305. # facing
  306. if self.keys["accel"]:
  307. heading_rad = DEG_TO_RAD * heading
  308. # This builds a new velocity vector and adds it to the current one
  309. # relative to the camera, the screen in Panda is the XZ plane.
  310. # Therefore all of our Y values in our velocities are 0 to signify
  311. # no change in that direction.
  312. newVel = \
  313. LVector3(sin(heading_rad), 0, cos(heading_rad)) * ACCELERATION * dt
  314. newVel += self.getVelocity(self.ship)
  315. # Clamps the new velocity to the maximum speed. lengthSquared() is
  316. # used again since it is faster than length()
  317. if newVel.lengthSquared() > MAX_VEL_SQ:
  318. newVel.normalize()
  319. newVel *= MAX_VEL
  320. self.setVelocity(self.ship, newVel)
  321. # Finally, update the position as with any other object
  322. self.updatePos(self.ship, dt)
  323. # Creates a bullet and adds it to the bullet list
  324. def fire(self, time):
  325. direction = DEG_TO_RAD * self.ship.getR()
  326. pos = self.ship.getPos()
  327. bullet = loadObject("bullet.png", scale=.2) # Create the object
  328. bullet.setPos(pos)
  329. # Velocity is in relation to the ship
  330. vel = (self.getVelocity(self.ship) +
  331. (LVector3(sin(direction), 0, cos(direction)) *
  332. BULLET_SPEED))
  333. self.setVelocity(bullet, vel)
  334. # Set the bullet expiration time to be a certain amount past the
  335. # current time
  336. self.setExpires(bullet, time + BULLET_LIFE)
  337. # Finally, add the new bullet to the list
  338. self.bullets.append(bullet)
  339. # We now have everything we need. Make an instance of the class and start
  340. # 3D rendering
  341. demo = AsteroidsDemo()
  342. demo.run()