roids.py

../_images/roids.png

Asteroids style game demo using polygons.

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