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()
1 Answer 1
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.
-
\$\begingroup\$ 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? \$\endgroup\$Marieke Pasker– Marieke Pasker2020年01月27日 14:31:02 +00:00Commented Jan 27, 2020 at 14:31
-
\$\begingroup\$ Probably you're applying the rotation action onto the objects at every tick of the game clock now. \$\endgroup\$Ignacio Vergara Kausel– Ignacio Vergara Kausel2020年01月27日 14:32:07 +00:00Commented Jan 27, 2020 at 14:32
-
\$\begingroup\$ I don't know where i am applying it again and again. :S I can't find it \$\endgroup\$Marieke Pasker– Marieke Pasker2020年01月27日 14:36:50 +00:00Commented Jan 27, 2020 at 14:36
-
\$\begingroup\$ That's beyond what I can help with. Undo your changes and start again carefully. \$\endgroup\$Ignacio Vergara Kausel– Ignacio Vergara Kausel2020年01月27日 14:53:07 +00:00Commented Jan 27, 2020 at 14:53