roids.py

../_images/roids.jpg

Test for polygons.

Asteroids style game demo using polygons.

Note

This example requires the following modules:

  • st7789py

  • tft_config

  • tft_buttons

  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()