I've been working on learning Pygame and I made a Breakout clone. I'm looking for stylistic advice and overall suggestions for improvement.
Controls are simple: - ← and → to move.
# Breakout/Arkanoid clone
import pygame, sys
from pygame.locals import *
import math
# General game attributes
FPS = 100
WINDOWWIDTH = 480
WINDOWHEIGHT = 600
MAXLEVEL = 6
# Ball attributes
BALLSTARTINGX = 0
BALLSTARTINGY = 250
BALLSPEED = 6 # pixels per tick
STARTINGANGLE = 60
# Paddle attributes
PADDLEWIDTH = 100
PADDLEHEIGHT = 10
PADDLESTARTINGX = (WINDOWWIDTH-PADDLEWIDTH)/2
PADDLEY = WINDOWHEIGHT-PADDLEHEIGHT
PADDLESPEED = 6
# Brick attributes
BRICKWIDTH = 50
BRICKHEIGHT = 25
SIDEBUFFER = 15
TOPBUFFER = 220
# Colors
WHITE = (255, 255, 255)
NAVYBLUE = ( 60, 60, 100)
RED = (200, 0, 0)
DARKRED = (100, 0, 0)
ORANGE = (200, 100, 0)
DARKORANGE = (100, 50, 0)
YELLOW = (200, 200, 0)
DARKYELLOW = (100, 100, 0)
GREEN = ( 0, 200, 0)
DARKGREEN = ( 0, 100, 0)
BLUE = ( 0, 0, 200)
DARKBLUE = ( 0, 0, 100)
PURPLE = (200, 0, 200)
DARKPURPLE = (100, 0, 100)
RAINBOW = [(RED, DARKRED),(ORANGE,DARKORANGE),(YELLOW,DARKYELLOW),
(GREEN,DARKGREEN),(BLUE,DARKBLUE),(PURPLE,DARKPURPLE)]
def main():
global FPSCLOCK, DISPLAYSURF, FONT
pygame.init()
# Globals
FPSCLOCK = pygame.time.Clock()
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
# Caption
pygame.display.set_caption("Bricky")
# Game objects
ball = Ball(DISPLAYSURF)
paddle = Paddle(DISPLAYSURF)
game_board = Game_board(DISPLAYSURF)
new_level = True
# Font
FONT = pygame.font.Font('freesansbold.ttf', 18)
# Text objects
text_newlevel_surf = FONT.render("New level!", True, WHITE, NAVYBLUE)
text_newlevel = text_newlevel_surf.get_rect()
text_newlevel.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)
text_youwin_surf = FONT.render("YOU WIN!", True, WHITE, NAVYBLUE)
text_youwin = text_newlevel_surf.get_rect()
text_youwin.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)
text_gamestart_surf = FONT.render("Game start!", True, WHITE, NAVYBLUE)
text_gamestart = text_newlevel_surf.get_rect()
text_gamestart.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)
text_youlose_surf = FONT.render("YOU LOSE!", True, WHITE, NAVYBLUE)
text_youlose = text_newlevel_surf.get_rect()
text_youlose.center = (WINDOWWIDTH/2, WINDOWHEIGHT/2)
# Main loop
while True:
DISPLAYSURF.fill(NAVYBLUE)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
elif event.type == KEYDOWN:
if(event.key == K_LEFT):
paddle.direction -= paddle.speed
elif(event.key == K_RIGHT):
paddle.direction += paddle.speed
elif event.type == KEYUP:
if(event.key == K_LEFT):
paddle.direction += paddle.speed
elif(event.key == K_RIGHT):
paddle.direction -= paddle.speed
if new_level:
game_board.new_board()
ball.posx, ball.posy = BALLSTARTINGX, BALLSTARTINGY
ball.angle = STARTINGANGLE
new_level = False
paddle.posx = PADDLESTARTINGX
bricks_cleared = len(game_board.brickarray) == 0
if bricks_cleared:
if game_board.roundnum == MAXLEVEL:
DISPLAYSURF.fill(NAVYBLUE)
DISPLAYSURF.blit(text_youwin_surf, text_youwin)
pygame.display.update()
pygame.time.wait(1000)
else:
new_level = True
DISPLAYSURF.fill(NAVYBLUE)
if game_board.roundnum == 0:
DISPLAYSURF.blit(text_gamestart_surf, text_gamestart)
else:
DISPLAYSURF.blit(text_newlevel_surf, text_newlevel)
pygame.display.update()
pygame.time.wait(1000)
game_board.roundnum += 1
elif ball.ballsremaining == -1:
DISPLAYSURF.fill(NAVYBLUE)
DISPLAYSURF.blit(text_youlose_surf, text_youlose)
pygame.display.update()
pygame.time.wait(2000)
new_level = True
game_board.roundnum = 0
ball.ballsremaining = 3
else:
paddle.update()
ball.update(paddle.posx, game_board.brickarray) # Handle ball collisions
game_board.update(ball.brickarray) # Destroy bricks colliding with ball
pygame.display.update()
FPSCLOCK.tick(FPS)
class Game_board(object):
def __init__(self, surface):
self.surf = surface
self.roundnum = 6
def draw_brick(self, x, y, color):
pygame.draw.rect(self.surf, color[0], (x,y,BRICKWIDTH, BRICKHEIGHT))
pygame.draw.rect(self.surf, color[1], (x,y,BRICKWIDTH,BRICKHEIGHT), 3)
def draw_board(self):
for brick in self.brickarray:
self.draw_brick(brick[1][0], brick[1][1], RAINBOW[brick[0][1]])
def new_board(self):
self.brick_indexes = zip(range(9)*self.roundnum, [i for i in range(self.roundnum) for _ in range(9)])
self.brick_positions = [(SIDEBUFFER + x * BRICKWIDTH, (TOPBUFFER-(y*BRICKHEIGHT*1.5))) for x,y in self.brick_indexes]
self.brickarray = zip(self.brick_indexes, self.brick_positions)
def update(self, brickarray):
self.brickarray = brickarray
self.draw_board()
class Paddle(Game_board):
def __init__(self, surf):
"""
Initialize paddle. Paddle starts in the middle. Speed is constant.
"""
self.surf = surf
self.posx, self.posy = PADDLESTARTINGX, PADDLEY
self.speed = PADDLESPEED
self.direction = 0
pygame.draw.rect(self.surf, WHITE,(self.posx, self.posy, PADDLEWIDTH, PADDLEHEIGHT))
def update(self):
"""
Move paddle if the left or right key is held down. Do not allow
paddle to move out of bounds.
"""
#############################
# Handling movement of paddle
#############################
self.posx += self.direction
##############################################
# Making sure paddle does not go out of bounds
##############################################
if self.posx < 0:
self.posx = 0
if self.posx > WINDOWWIDTH-PADDLEWIDTH:
self.posx = WINDOWWIDTH-PADDLEWIDTH
# Drawing paddle
pygame.draw.rect(self.surf, WHITE,(self.posx, self.posy, PADDLEWIDTH, PADDLEHEIGHT))
class Ball(object):
"""
Ball object for Bricky game. Handles collision detection and bouncing.
"""
def __init__(self, surf):
"""
Initialize ball. Ball always starts in same position with same
angle. Speed is constant throughout the game.
"""
self.posx,self.posy = BALLSTARTINGX,BALLSTARTINGY
self.speed = BALLSPEED
self.angle = STARTINGANGLE
self.surf = surf
self.ballsremaining = 3
pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)
def update(self, paddlex, brickarray):
self.brickarray = brickarray
"""
Calculate position of ball at next tick. If no collision occurs,
ball's position will change based on current angle and speed.
If ball bounces off of a brick or a wall, position will not
change this tick but angle will update to reflect the direction
the ball is facing, based on its angle before the collision.
If ball bounces off of the paddle, again position will not
change but angle will update based off of the location on the
paddle that the ball hit: the closer to the center the ball hits,
the closer to a vertical angle the ball will have. Hitting towards
the edges will give the ball a steeper angle closer to
0 or 180.
"""
############################
# Displaying remaining balls
############################
text_ballsremaining_surf = FONT.render("Balls left: {}".format(self.ballsremaining), True, WHITE, NAVYBLUE)
text_ballsremaining = text_ballsremaining_surf.get_rect()
text_ballsremaining.center = (55, 10)
self.surf.blit(text_ballsremaining_surf, text_ballsremaining)
######################################
# Next position if no collision occurs
######################################
# Next position depends on current angle and speed
xdif = int(math.cos(self.angle*(math.pi/180)) * self.speed)
ydif = int(math.sin(self.angle*(math.pi/180)) * self.speed)
########################
# Bouncing off of bricks
########################
ycollission = [ y1 < (self.posy + ydif) < y2 for y1, y2 in [ (y, y+BRICKHEIGHT) for x,y in [ b[1] for b in self.brickarray ] ]]
xcollission = [ x1 < (self.posx + xdif) < x2 for x1, x2 in [ (x, x+BRICKWIDTH) for x,y in [ b[1] for b in self.brickarray ] ]]
collissions = [ xcollission[i] and ycollission[i] for i in range(len([ b[1] for b in self.brickarray ]))]
if any(collissions):
collided = self.brickarray.pop([ i for i in range(len(collissions)) if collissions[i] ][0])[1]
if (self.posx <= collided[0]) and (collided[0] <= (self.posx + xdif) <= collided[0]+BRICKWIDTH):
if self.angle < 90:
adj = (90 - (self.angle%90))*2 # Change angle clockwise
else:
adj = -(self.angle%90)*2 # change angle counter-clockwise
elif (self.posx >= (collided[0]+BRICKWIDTH)) and (collided[0] <= (self.posx + xdif) <= collided[0]+BRICKWIDTH):
if self.angle < 180:
adj = -(self.angle%90)*2 # Change angle counter-clockwise
else:
adj = (90 - (self.angle%90))*2 # Change angle clockwise
elif (self.posy <= collided[1]) and (collided[1] <= (self.posy + ydif) <= collided[1]+BRICKHEIGHT):
if self.angle < 90:
adj = -(self.angle%90)*2 # Change angle counter-clockwise
else:
adj = (90 - (self.angle%90))*2 # Change angle clockwise
elif (self.posy >= collided[1]+BRICKHEIGHT) and (collided[1] <= (self.posy + ydif) <= collided[1]+BRICKHEIGHT):
if self.angle < 270:
adj = -(self.angle%90)*2 # Change angle counter-clockwise
else:
adj = (90 - (self.angle%90))*2 # Change angle clockwise
self.angle = (self.angle + adj) % 360
##########################################
# Bouncing against window borders
##########################################
if not (0 < (self.posx + xdif) < WINDOWWIDTH): #Bounce off wall
if (90 <= self.angle <= 180) or (270 <= self.angle <= 360):
adj = -(self.angle%90)*2 # Change angle counterclockwise
else:
adj = (90 - (self.angle%90))*2 # Change angle clockwise
self.angle = (self.angle + adj) % 360
elif self.posy + ydif < 0: # Bounce off ceiling
if (90 <= self.angle <= 180) or (270 <= self.angle <= 360):
adj = (90 - (self.angle%90))*2 # Change angle clockwise
else:
adj = -(self.angle%90)*2 # Change angle counterclockwise
self.angle = (self.angle + adj) % 360
##############################################
# Bouncing off paddle or falling out of bounds
##############################################
elif WINDOWHEIGHT > self.posy + ydif > WINDOWHEIGHT-PADDLEHEIGHT: # Bounce off paddle or fall out of bounds
if paddlex < self.posx < paddlex+PADDLEWIDTH:# Bounce off paddle
if (270 <= self.angle <= 360):
adj = (90 - (self.angle%90))*2 # Change angle clockwise
else:
adj = -(self.angle%90)*2 # Change angle counterclockwise
#####
# Angle changes based off of how far from the middle the ball has bounced
#####
paddleangle = (paddlex - self.posx) + PADDLEWIDTH/2
paddleangle *= 130.0/PADDLEWIDTH
self.angle = 270 - paddleangle
else: # Ball falls out of bounds
self.posx += xdif
self.posy += ydif
pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)
########################################
# Movement if ball does not hit anything
########################################
else:
self.posx += xdif
self.posy += ydif
pygame.draw.circle(self.surf, WHITE, (self.posx,self.posy), 5, 0)
if self.posy > WINDOWHEIGHT + 300:
self.posx,self.posy = BALLSTARTINGX, BALLSTARTINGY
self.angle = STARTINGANGLE
self.ballsremaining -= 1
if __name__ == "__main__":
main()