roids.py
Asteroids style game demo using polygons.
Note
This example requires the following modules:
|
|
|
1"""
2roids.py
3========
4
5.. figure:: ../_static/roids.jpg
6 :align: center
7
8 Test for polygons.
9
10Asteroids style game demo using polygons.
11
12.. note:: This example requires the following modules:
13
14 .. hlist::
15 :columns: 3
16
17 - `st7789py`
18 - `tft_config`
19 - `tft_buttons`
20
21"""
22
23import math
24import random
25import utime
26import tft_config
27import tft_buttons as Buttons
28import st7789py as st7789
29
30
31tft = tft_config.config(tft_config.WIDE)
32buttons = Buttons.Buttons()
33
34
35def main():
36 '''
37 Game on!
38 '''
39
40 class Poly():
41 '''
42 Poly class to keep track of a polygon based sprite
43 '''
44
45 def __init__(
46 self,
47 # list (x,y) tuples of convex polygon, must be closed
48 polygon,
49 x=None, # x location of polygon
50 y=None, # y location of polygon
51 v_x=None, # velocity in x axis
52 v_y=None, # velocity in y axis
53 angle=None, # angle in radians polygon is facing
54 spin=None, # spin in radians per frame_time
55 scale=None, # scale factor for polygon
56 radius=None, # radius of polygon for collision detection
57 max_velocity=10, # max velocity of polygon
58 counter=0):
59
60 # scale the polygon if scale was given
61 self.polygon = (
62 polygon if scale is None else [(int(scale*x[0]), int(scale*x[1])) for x in polygon])
63
64 # if no location given assign a random location
65 self.x = random.randint(0, width) if x is None else x
66 self.y = random.randint(0, width) if y is None else y
67
68 # set angle if given
69 self.angle = float(0) if angle is None else angle
70
71 # set random spin unless one was given
72 self.spin = random.randint(-3, 3) / 16 if spin is None else spin
73
74 # set random velocity unless one was given
75 self.velocity_x = random.uniform(
76 0.50, 0.99)*6-3 + 0.75 if v_x is None else v_x
77 self.velocity_y = random.uniform(
78 0.50, 0.99)*6-3 + 0.75 if v_y is None else v_y
79
80 # set radius, max_velocity and radius counter
81 self.radius = radius
82 self.max_velocity = max_velocity
83 self.counter = counter
84
85 def rotate(self, rad):
86 '''
87 Rotate polygon in radians
88 '''
89 self.angle += rad
90 if self.angle > rad_max:
91 self.angle = 0
92 elif self.angle < 0:
93 self.angle = rad_max
94
95 def move(self):
96 '''
97 Rotate and move polygon velocity distance.
98 '''
99 if self.spin != 0:
100 self.rotate(self.spin)
101
102 self.x += int(self.velocity_x)
103 self.y += int(self.velocity_y)
104 self.x %= width
105 self.y %= height
106
107 def draw(self, color):
108 '''
109 Draw the polygon
110 '''
111 tft.polygon(self.polygon, self.x, self.y, color, self.angle, 0, 0)
112
113 def collision(self, poly):
114 '''
115 Detect collisions using overlapping radiuses.
116 Returns True on collision.
117 '''
118 return abs(
119 ((self.x - poly.x) * (self.x - poly.x) +
120 (self.y - poly.y) * (self.y - poly.y))
121 < (self.radius + poly.radius) * (self.radius + poly.radius))
122
123 def create_roid(size, x=None, y=None, v_x=None, v_y=None):
124 '''
125 Create a new roid with the given parameters.
126 '''
127 return Poly(
128 roid_poly,
129 x=x,
130 y=y,
131 v_x=v_x,
132 v_y=v_y,
133 scale=roid_scale[size],
134 radius=roid_radius[size],
135 counter=size)
136
137 def update_missiles():
138 '''
139 Update active missiles and handle asteroid hits.
140 '''
141 for missile in list(missiles): # for each missile
142 missile.draw(st7789.BLACK) # erase old missile
143 if missile.counter > 0: # if counter > 0 missile is active
144 missile.move() # update missile position
145
146 for roid in list(roids): # for each roid
147 if missile.collision(roid): # check if missile collides with roid
148 roid.draw(st7789.BLACK) # erase the roid
149 if roid.counter > 0: # if roid is not the smallest size
150 roids.append( # add first smaller roid
151 create_roid(
152 roid.counter-1,
153 x=roid.x,
154 y=roid.y,
155 v_x=roid.velocity_x,
156 v_y=roid.velocity_y))
157 roids.append( # add second smaller roid
158 create_roid(
159 roid.counter-1,
160 x=roid.x,
161 y=roid.y,
162 v_x=-roid.velocity_x,
163 v_y=-roid.velocity_y))
164
165 roids.remove(roid) # remove the roid that was hit
166 missile.counter = 0
167
168 if missile.counter > 0: # if the missile has life left
169 missile.draw(st7789.WHITE) # draw missile
170 missile.counter -= 1 # reduce missile life
171 else:
172
173 missiles.remove(missile) # remove exploded missile
174 else:
175 missiles.remove(missile) # remove expired missile
176
177 def update_ship():
178 '''
179 Update ship velocity and limit to max_velocity
180 '''
181 # apply drag to velocity of ship so it will eventually slow to a stop
182 ship.velocity_x -= ship.velocity_x * ship_drag_frame
183 ship.velocity_y -= ship.velocity_y * ship_drag_frame
184
185 if ship.velocity_x > ship.max_velocity: # Limit velocity to +/- max_velocity
186 ship.velocity_x = ship.max_velocity
187 elif ship.velocity_x < -ship.max_velocity:
188 ship.velocity_x = -ship.max_velocity
189
190 if ship.velocity_y > ship.max_velocity:
191 ship.velocity_y = ship.max_velocity
192 elif ship.velocity_y < -ship.max_velocity:
193 ship.velocity_y = -ship.max_velocity
194
195 if abs(ship.velocity_x) < 0.1: # if ship is moving very slowly, stop it
196 ship.velocity_x = 0.0
197
198 if abs(ship.velocity_y) < 0.1:
199 ship.velocity_y = 0.0
200
201 ship.move() # move the ship and draw it
202 ship.draw(st7789.WHITE)
203
204 def update_roids():
205 '''
206 Update roid positions handle ship collisions
207 Returns True if not hit, False if hit
208 '''
209 not_hit = True
210
211 for roid in roids: # for each roid, erase, move then draw
212 roid.draw(st7789.BLACK)
213 roid.move()
214 roid.draw(st7789.WHITE)
215
216 if roid.collision(ship): # check for roid/ship collision
217 ship.draw(st7789.BLACK) # erase ship
218 ship.velocity_x = 0.0 # stop movement
219 ship.velocity_y = 0.0
220
221 not_hit = False
222
223 return not_hit
224
225 def explode_ship():
226 '''
227 Increment explosion step and alternate between drawing
228 explosion poly and explosion poly rotated 45 degrees
229
230 Returns True when explosion is finished
231 '''
232 ship.counter += 1
233 if ship.counter % 2 == 0:
234 tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK, 0.785398)
235 tft.polygon(explosion_poly, ship.x, ship.y, st7789.WHITE)
236 else:
237 tft.polygon(explosion_poly, ship.x, ship.y, st7789.WHITE, 0.785398)
238 tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK)
239
240 if ship.counter > 25:
241 # erase explosion, move ship to center and stop explosion
242 tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK)
243 # move ship to center
244 ship.x = width >> 1
245 ship.y = height >> 1
246 ship.counter = 0
247 return True
248
249 return False
250
251 # enable display and clear screen
252 tft.fill(st7789.BLACK)
253 width = tft.width
254 height = tft.height
255
256 rad_max = 2 * math.pi # 360' in radians
257
258 # ship variables
259 ship_alive = True
260 ship_radius = 7
261 ship_rad_frame = rad_max / 16 # turning rate per frame
262 ship_accel_frame = 0.6 # acceleration per frame
263 ship_drag_frame = 0.015 # drag factor per frame
264
265 ship_poly = [(-7, -7), (7, 0), (-7, 7), (-3, 0), (-7, -7)]
266
267 ship = Poly(
268 ship_poly,
269 x=width >> 1,
270 y=height >> 1,
271 v_x=0,
272 v_y=0,
273 radius=ship_radius,
274 spin=0.0)
275
276 explosion_poly = [(-4, -4), (-4, 4), (4, 4), (4, -4), (-4, -4)]
277
278 # asteroid variables
279 roid_radius = [5, 10, 16]
280 roid_scale = [0.33, 0.66, 1.0]
281
282 roid_poly = [
283 (-5, -15), (-2, -13), (11, -14), (15, -7), (14, 0),
284 (16, 5), (11, 16), (7, 16), (-7, 14), (-14, 7),
285 (-13, 1), (-14, -8), (-11, -15), (-5, -15)]
286 roids = []
287
288 # missile variables
289 missile_velocity = 8
290 missile_max = 8
291 missile_life = 20
292 missile_rate = 200
293 missile_last = utime.ticks_ms()
294 missile_poly = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
295 missiles = []
296
297 frame_time = 60 # target frame rate delay
298
299 # game loop
300 while True:
301 last_frame = utime.ticks_ms()
302
303 # add roids if there are none
304 if len(roids) == 0:
305 roids = [create_roid(2), create_roid(2)]
306
307 update_missiles()
308
309 # Erase the ship
310 ship.draw(st7789.BLACK)
311
312 if ship_alive:
313 # if left button pressed
314 if buttons.left and buttons.left.value() == 0:
315 # rotate ship counter clockwise
316 ship.rotate(-ship_rad_frame)
317
318 # if right button pressed
319 if buttons.right and buttons.right.value() == 0:
320 # rotate ship clockwise
321 ship.rotate(ship_rad_frame)
322
323 # if hyperspace button pressed move ship to random location
324 if buttons.hyper and buttons.hyper.value() == 0:
325 diameter = ship.radius * 2
326 ship.x = random.randint(diameter, width - diameter)
327 ship.y = random.randint(diameter, height - diameter)
328
329 # stop movement
330 ship.velocity_x = 0.0
331 ship.velocity_y = 0.0
332
333 # if thrust button pressed
334 if buttons.thrust and buttons.thrust.value() == 0:
335 # accelerate ship in the direction the ship is facing
336 d_y = math.sin(ship.angle) * ship_accel_frame
337 d_x = math.cos(ship.angle) * ship_accel_frame
338 ship.velocity_x += d_x
339 ship.velocity_y += d_y
340
341 # if the fire button is pressed and less than missile_max active missles
342 if buttons.fire and buttons.fire.value() == 0 and len(missiles) < missile_max:
343
344 # limit missiles firing to once every missile_rate ms
345 if last_frame - missile_last > missile_rate:
346
347 # fire missile in direction ship in facing
348 v_y = math.sin(ship.angle) * missile_velocity
349 v_x = math.cos(ship.angle) * missile_velocity
350
351 # create new missile
352 missile = Poly(
353 missile_poly,
354 x=ship.x,
355 y=ship.y,
356 v_x=v_x,
357 v_y=v_y,
358 angle=ship.angle,
359 radius=1,
360 spin=0.0,
361 counter=missile_life)
362
363 # add to to missile list and save last fire time
364 missiles.append(missile)
365 missile_last = last_frame
366
367 update_ship()
368
369 else:
370 # explosion animation until returns True
371 ship_alive = explode_ship()
372
373 # update roids and return if ship was not hit
374 not_hit = update_roids()
375 if ship_alive:
376 ship_alive = not_hit
377
378 # wait until frame time expires
379 while utime.ticks_ms() - last_frame < frame_time:
380 pass
381
382
383main()