# Asteroids using pygame

I made the looks of asteroids using pygame. I still need to implement collisions but since it is running incredibly slow with 'amount' anything above 10. Please rip my code to shreds and explain why it is bad so that I can improve. :)

import pygame
import pygame.locals
import numpy
import random

asteroids = []
paused = False
delta_angle = 0.01
amount = 5 # Anything above 5 makes it run much slower, above 10 makes it run really slow.
maxVelocity = 10
screen = (1600, 1200)
left_down, right_down = False, False
boost = False

pygame.init()
display = pygame.display.set_mode(screen, pygame.RESIZABLE)
background = pygame.Surface(screen)
clock = pygame.time.Clock()

class Boid:
def __init__(self):
self.position = numpy.array([0, 0])
self.velocity = numpy.array([0, 0])
self.acceleration = numpy.array([0, 0])
self.angle = 0
self.size = 1

def update(self):
self.position += self.velocity
self.velocity += self.acceleration
self.acceleration = 0

def rotation(angle):
rotation = numpy.array([[numpy.cos(angle), -numpy.sin(angle)], [numpy.sin(angle), numpy.cos(angle)]])
return rotation

def Ship(angle):
offset1 = numpy.array([[0.0, -15.0], [-10.0, 15.0], [0.0, 10.0], [10.0, 15.0], [-5.0, 12.5], [0.0, 20.0], [5.0, 12.5]])
ship = []

for i in range(len(offset1)):
offset1[i] = rotation(angle).dot(offset1[i])
ship.append(offset1[i])

return ship

def Asteroids(angle):
offset2 = numpy.array([[-10.0, 0.0], [-8.0, -5.0], [-5.0, -8.0], [0.0, -10.0], [6.0, -8.0], [9.0, -4.0], [4.0, -2.0], [10.0, 0.0], [7.0, 7.5], [2.5, 4.0], [0.0, 10.0], [-6.0, 8.0], [-9.0, 3.0], [-4.0, 0.0]])
asteroid = []

for i in range(len(offset2)):
offset2[i] = rotation(angle).dot(offset2[i])
asteroid.append(offset2[i])

return asteroid

def player_creator():
boid = Boid()
vec = numpy.random.random_sample(2) - numpy.array([0.5, 0.5])
boid.position = numpy.array([screen[0] / 2, screen[1] / 2])
boid.velocity = vec / numpy.linalg.norm(vec) * 5
boid.acceleration = numpy.array([0.0, 0.0])
if vec[0] < 0 and vec[1] > 0 or vec[0] < 0 and vec[1] < 0:
boid.angle = numpy.arctan((vec[1]) / (vec[0])) + numpy.pi
elif vec[0] > 0 and vec[1] < 0:
boid.angle = numpy.arctan((vec[1]) / (vec[0])) + 2 * numpy.pi
else:
boid.angle = numpy.arctan((vec[1]) / (vec[0]))

return boid

def drawing_player_creator():
vertices = player.position + player.velocity + numpy.array([Ship(angle)[0], Ship(angle)[1], Ship(angle)[2], Ship(angle)[3]])
pygame.draw.polygon(display, (0, 0, 0), (vertices), 0)
pygame.draw.polygon(display, (255, 255, 255), (vertices), 2)
player.update()

def drawing_booster_flames():
vertices = player.position + numpy.array([Ship(angle)[2], Ship(angle)[4], Ship(angle)[5], Ship(angle)[6]])
pygame.draw.polygon(display, (0, 0, 0), (vertices), 0)
pygame.draw.polygon(display, (255, 255, 255), (vertices), 2)

def asteroid_creator(amount):
for asteroid in range(amount):
asteroid = Boid()
vec = numpy.random.random_sample(2) - numpy.array([0.5, 0.5])
asteroid.position = numpy.random.random_sample(2) * screen
asteroid.velocity = vec / numpy.linalg.norm(vec)
asteroid.acceleration = numpy.array([0.0, 0.0])
asteroid.angle = numpy.random.random_sample(1)[0] * 2 * numpy.pi
asteroid.size = numpy.random.randint(2, 8)
asteroids.append(asteroid)

def drawing_asteroid_creator(amount):
for n in range(amount):
#pygame.draw.circle(display, (255 - (n / amount * 255), 255 - (n / amount * 255), 255 - (n / amount * 255)), (int(asteroids[n].position[0]), int(asteroids[n].position[1])), 21)
#pygame.draw.circle(display, (n / amount * 255, n / amount * 255, n / amount * 255), (int(asteroids[n].position[0]), int(asteroids[n].position[1])), 19)
asteroid_angle = asteroids[n].angle
vertices =  asteroids[n].position + asteroids[n].size * numpy.array([(Asteroids(asteroid_angle)[0]), (Asteroids(asteroid_angle)[1]), (Asteroids(asteroid_angle)[2]), (Asteroids(asteroid_angle)[3]), (Asteroids(asteroid_angle)[4]), (Asteroids(asteroid_angle)[5]), (Asteroids(asteroid_angle)[6]), (Asteroids(asteroid_angle)[7]), (Asteroids(asteroid_angle)[8]), (Asteroids(asteroid_angle)[9]), (Asteroids(asteroid_angle)[10]), (Asteroids(asteroid_angle)[11]), (Asteroids(asteroid_angle)[12]), (Asteroids(asteroid_angle)[13])])
pygame.draw.polygon(display, (0, 0, 0), (vertices), 0)
pygame.draw.polygon(display, (255, 255, 255), (vertices), 2)
asteroids[n].update()

asteroid_creator(amount)
player = player_creator()
angle = player.angle + numpy.pi / 2

while True:

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()

if event.type == pygame.KEYDOWN:
if event.key == pygame.K_p:
if paused == False:
paused = True
elif paused == True:
paused = False
if event.key == pygame.K_LEFT:
left_down = True
if event.key == pygame.K_RIGHT:
right_down = True
if event.key == pygame.K_SPACE:
boost = True

if event.type == pygame.KEYUP:
if event.key == pygame.K_LEFT:
left_down = False
if event.key == pygame.K_RIGHT:
right_down = False
if event.key == pygame.K_SPACE:
boost = False
if event.type == pygame.VIDEORESIZE:
screen = (event.w, event.h)
display = pygame.display.set_mode((screen), pygame.RESIZABLE)
background = pygame.Surface(screen)

if paused == False:
display.blit(background, (0, 0))
drawing_asteroid_creator(amount)
drawing_player_creator()

if left_down == True:
angle -= delta_angle * 2 * numpy.pi
#player.velocity = rotation(-delta_angle * 2 * numpy.pi).dot(player.velocity)
if right_down == True:
angle += delta_angle * 2 * numpy.pi
#player.velocity = rotation(delta_angle * 2 * numpy.pi).dot(player.velocity)
if boost == True:
drawing_booster_flames()
player.velocity += rotation(-numpy.pi / 2).dot(numpy.array([numpy.cos(angle), numpy.sin(angle)]) / 10)
if maxVelocity < numpy.linalg.norm(player.velocity):
player.velocity = maxVelocity * player.velocity / numpy.linalg.norm(player.velocity)

if player.position[0] > screen[0] + 15:
player.position[0] = 0
if player.position[0] < 0 - 15:
player.position[0] = screen[0]

if player.position[1] > screen[1] + 15:
player.position[1] = 0 - 15
if player.position[1] < 0 - 15:
player.position[1] = screen[1] + 15

for n in range(amount):
if asteroids[n].position[0] > screen[0] + asteroids[n].size * 15:
asteroids[n].position[0] = 0 - asteroids[n].size * 15
if asteroids[n].position[0] < 0 - asteroids[n].size * 15:
asteroids[n].position[0] = screen[0] + asteroids[n].size * 15

if asteroids[n].position[1] > screen[1] + asteroids[n].size * 15:
asteroids[n].position[1] = 0 - asteroids[n].size * 15
if asteroids[n].position[1] < 0 - asteroids[n].size * 15:
asteroids[n].position[1] = screen[1] + asteroids[n].size * 15

clock.tick(120)
pygame.display.update()


I'll focus mostly on the performance issue and only hint at a few design ones at the end.

For this use-case, I'd say stay away from numpy. numpy has some pretty high overhead, which you tend to amortize by faster computations at a "massive" scale. Which is not the case here.

You use numpy for vector addition and for trigonometric functions. In the latter case, you can see that using numpy is the wrong approach. A quick benchmark:

In [5]: import numpy as np

In [6]: %timeit np.cos(0.6)
630 ns ± 19.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [7]: import math

In [8]: %timeit math.cos(0.6)
70.7 ns ± 1.47 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [9]: from math import cos

In [10]: %timeit cos(0.6)
44.1 ns ± 0.51 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)



numpy is ~10x slower than pure Python, and importing the function locally which gives you an extra bump.

For vectors of length 2, probably the overhead of using it is way higher than the computational wins. That's even more when you apply a trigonometric function to a single value (scalar).

If you still want to go on with numpy you'd most likely want to focus on the rotation function.

def rotation(angle):
rotation = numpy.array([[numpy.cos(angle), -numpy.sin(angle)], [numpy.sin(angle), numpy.cos(angle)]])
return rotation


Here you calculate twice each trigonometric function used. you could just calculate each once and reuse it

def rotation_reuse(angle):
c, s = numpy.cos(angle), numpy.sin(angle)
return numpy.array(((c,-s), (s, c)))


Again, benchmarking this change

In [17] %timeit rotation(0.6)
4.3 µs ± 252 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [18]: %timeit rotation_reuse(0.6)
2.68 µs ± 30.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)


You can see that it takes basically half the time, as you could expect by doing half the amount of operations.

And if we continue with what we learned already

In [20]: def new_rotation(angle):
...:     c, s = cos(angle), sin(angle)
...:     return numpy.array(((c,-s), (s, c)))
...:

In [21]: %timeit new_rotation(0.6)
1.22 µs ± 18.7 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)


Another 2x improvement, while keeping a numpy array as return value. This won't break the rest of your operations with the positions and velocities. You'd have to check if doing a manual loop to do those is faster or not than using numpy.

Furthermore, on a more "design" level, you have a lot of duplicated code between the Ship and Asteroid functions. Here, you most likely should try to have a class and then create instances, and thus reducing the amount of duplication.

Also, in Python, people tend to write class definitions with the first letter uppercase and functions all lowercase. You have Ship and Asteroid as functions. This ties with the previous paragraph, maybe you already kinda realized that those things are proper entities that deserve a more substantial modeling.

Moreover, improve the doctrings of the functions. As it is now, it's rather hard for a casual reader to understand what's happening.

• Thanks for the feedback! I implemented the single vertices calculator but i think i made a mistake because all the objects now spin like crazy. Do you know why that might be? – Marieke Pasker Jan 27 '20 at 14:31
• Probably you're applying the rotation action onto the objects at every tick of the game clock now. – Ignacio Vergara Kausel Jan 27 '20 at 14:32
• I don't know where i am applying it again and again. :S I can't find it – Marieke Pasker Jan 27 '20 at 14:36
• That's beyond what I can help with. Undo your changes and start again carefully. – Ignacio Vergara Kausel Jan 27 '20 at 14:53
• I did, thank you. :) – Marieke Pasker Jan 27 '20 at 15:11