main.py 18 KB

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