6
\$\begingroup\$

This is my second game, not yet finished but working. I've put so much time into it I wanted to get feedback before continuing. I always learn so much from those that comment.

Whilst my first (still a work in progress), is object orientated, this isn't. Given that though, I certainly appreciate the OO method more now.

Please try the game out and post your feedback:

# Space Invaders
# By Dave
#
# A simple Space Invaders clone
# 
# Planned features not yet implemented:
# - bonus dropped if strobing invader hit
# - high scores tracking using pickled data
# - explosion effects (drawn using colored pixels?)
# - more....

import math
import pygame
import random
import sys
from itertools import cycle
from datetime import datetime
from pygame import gfxdraw
from pygame.locals import *

def print_text(surface, font, text, surf_rect, x = 0, y = 0, center = False,\
               color = (255, 255, 255)):
    """
    Draws text onto a surface. If center, text is centered on screen at y
    """
    if not center:
        textimage = font.render(text, True, color)
        surface.blit(textimage, (x, y))
    else:
        textimage = font.render(text, True, color)
        text_rect = textimage.get_rect()
        x = (surf_rect.width // 2) - (text_rect.width // 2 )
        surface.blit(textimage, (x, y))

def game_is_over(surface, font, ticks):
    timer = ticks
    surf_rect = surface.get_rect()
    surf_height = surf_rect.height
    surf_width = surf_rect.width
    print_text(screen, font, "G A M E  O V E R", surf_rect, y = 260,\
               center = True)
    pygame.display.update()
    while True:
        ticks = pygame.time.get_ticks()
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
        if ticks > timer + 3000:
            break

def next_level(level):
    level += 1
    if level > 6:
        level = 6
    return level

def load_level(level):
    # create and populate(not all) lists
    invaders, colors = [], []

    start_intx, end_intx, increment_intx = 85, 725, 40 # 85, 725
    start_inty, end_inty, increment_inty = 60, 60, 30 #
    end_inty = end_inty + level * 30 # rows invaders = level number
    color_val = 256 / end_inty # ensure no color repetition
    for x in range(start_intx, end_intx, increment_intx):
        for y in range(start_inty, end_inty, increment_inty):
            invaders.append(pygame.Rect(x, y, 30, 15))
            colors.append(((x * 0.35) % 256, (y * color_val) % 256))

    return invaders, colors, len(invaders)

def draw_title_invader():
    rect = Rect(285,247,230,115)
    rect_width = 230
    a = 71
    b = 171
    pygame.draw.rect(backbuffer, (150,a, b),rect)
    # left eye
    pygame.draw.circle(backbuffer, BLACK, (rect.x+46,rect.y+30), 23)
    #right eye
    pygame.draw.circle(backbuffer, BLACK,(rect.x+rect_width-46,rect.y+30)\
                                          ,23)
    # left antennae
    pygame.draw.line(backbuffer, (150, a, b),(rect.x+115, rect.y),\
                     (rect.x+50, rect.y-55),2 )
    # right antennae
    pygame.draw.line(backbuffer,(150, a, b), (rect.x+ rect_width - 113,\
                            rect.y),(rect.x + rect_width-50, rect.y-55),2)
    # left side mouth
    pygame.draw.line(backbuffer, BLACK, (rect.x+46, rect.y+92),\
                        (rect.x + 115, rect.y + 61), 2)
    # right side mouth
    pygame.draw.line(backbuffer, BLACK, (rect.x+rect_width-46,\
                        rect.y+92), (rect.x+rect_width-115,\
                        rect.y+61), 2)

def draw_bonus_invader(i, bonus_color, bx, bonus_x):
    if i == 0:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
    if i == 1:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
    if i == 2:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
    if i == 3:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
    if i == 4:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
    if i == 5:
        pygame.draw.circle(backbuffer, bonus_color,
                           (bonus_invader.x+bx,bonus_invader.y+7),2)
        bx = next(bonus_x) # skip a color(ie it's not drawn)to move
                           # light sequence up ship


def draw_invader(backbuffer, rect, a, b, animate_invaders, ticks,\
                 animation_time):
    invader_width = 30
    # draw invader
    pygame.draw.rect(backbuffer, (150, a, b), rect)
    # left eye
    pygame.gfxdraw.filled_circle(backbuffer, rect.x + 6, rect.y + 4, 3, \
                                 BLACK)
    #right eye
    pygame.gfxdraw.filled_circle(backbuffer, rect.x + invader_width - 7,\
                                 rect.y + 4, 3, BLACK)
    # left antennae
    pygame.gfxdraw.line(backbuffer, rect.x + 14, rect.y, rect.x + 8,\
                       rect.y - 6, (150, a, b))
    # right antennae
    pygame.gfxdraw.line(backbuffer, rect.x + invader_width - 15, rect.y,\
                        rect.x + invader_width - 8, rect.y - 6, (150, a, b))

    # draw 'animation' if required
    if animate_invaders:
        pygame.gfxdraw.filled_trigon(backbuffer, rect.x+6, rect.y + 12,\
                                     rect.x + 14, rect.y + 4, rect.x +\
                                     invader_width - 7, rect.y + 12, BLACK)
    else:
        # left side mouth
        pygame.gfxdraw.line(backbuffer, rect.x + 6, rect.y + 12,\
                            rect.x + 15, rect.y + 8, BLACK)
        # right side mouth
        pygame.gfxdraw.line(backbuffer, rect.x + invader_width - 7,\
                            rect.y + 12, rect.x + invader_width - 15,\
                            rect.y + 8, BLACK)
    # ensure trigon is drawn for more than just a frame
    if ticks > animation_time + 200:
        animate_invaders = False

    return animate_invaders

##def load_sound(file):
##    return pygame.mixer.Sound(file)

pygame.init()
pygame.mixer.init() # not always called by pygame.init()

screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Space Invaders")
fpsclock = pygame.time.Clock()

#get screen metrics
the_screen = screen.get_rect()
screen_width = the_screen.width
screen_height = the_screen.height

backbuffer = pygame.Surface((the_screen.width, the_screen.height))

# fonts
font1 = pygame.font.SysFont(None, 30)
font2 = pygame.font.SysFont("Impact", 54)
font3 = pygame.font.SysFont("Impact", 36)

### load sounds
##space_voiceover = load_sound("SpaceInvadersIntro.wav")
##missile_sound = load_sound("missile.wav")

### play voiceover on startup
##space_voiceover.play()

# User event frequencies
RELOAD_SPEED = 400
MOVE_SIDEWAYS = 1000
MOVE_DOWN = 1000
BONUS_FREQ = 10000
INV_SHOOT_FREQ = 500

# create user events
move_invaders_sideways = pygame.USEREVENT + 1
move_invaders_down = pygame.USEREVENT + 2
reload = pygame.USEREVENT + 3
invader_shoot = pygame.USEREVENT + 4
bonus = pygame.USEREVENT + 5

# event timers
pygame.time.set_timer(move_invaders_down, 0) 
pygame.time.set_timer(move_invaders_sideways, MOVE_SIDEWAYS) 
pygame.time.set_timer(reload, RELOAD_SPEED)
pygame.time.set_timer(invader_shoot, INV_SHOOT_FREQ) 
pygame.time.set_timer(bonus, BONUS_FREQ)

# colors
BLACK = (0,0,0)
WHITE = (255,255,255)
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
YELLOW = (255,255,0)
DIMGRAY = (105,105,105)

shots, invader_shots, inv_shot_colors, bonus_invaders = [], [], [], []

# create player ship        
player = Rect(380,578,42,20)
player_gun = Rect(player.x + 18,player.y - 4, 6, 4)

# make screen rect for purposes of text-centering etc
the_screen = screen.get_rect()

# invader animation variables
animation_time = 0
animate_invaders = False
invader_width = 30
invader_height = 15

# flashing text vars
the_text = cycle(["Press Enter To Play, Earthling...", ""])
insert = next(the_text)
flash_timer = 0

# flashing bonus item vars
y1,y2,y3,y4,y5,y6 = (255,255,0), (225,225,0), (195,195,0), (165,165,0),\
                    (135,135,0), (105,105,0)
bonus_colors = cycle([y1,y2,y3,y4,y5,y6])
bonus_color = next(bonus_colors)
bonus_x = cycle([4,11,18,25,32,39]) # change draw x coord
bonus_timer = 0 # used to control frequency of changes

# vars for moving invaders down
move_right, move_down, reloaded = True, True, True
vert_steps = 0
side_steps = 0
moved_down = False
invaders_paused = False

invaders = 0 # prevents error until list is created
initial_invaders = 0 # use to manage freq of inv shots as invaders removed
shoot_level = 1 # manage freq of shots

# various gameplay variables
game_over = True
score = 0
lives = 2
level = 0
playing = False

# event loop
while True:
    ticks = pygame.time.get_ticks()
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()

        if event.type == KEYUP:
            if event.key == pygame.K_1 and not game_over:
                print("Next level")

        if event.type == invader_shoot and not game_over:
            i = random.randint(0, len(invaders)-1)
            shot_from = invaders[i]
            a, b = colors[i]
            invader_fired = True
            invader_shots.append(Rect(shot_from.x, shot_from.y, 5, 7))
            inv_shot_colors.append((150, a, b))

        if event.type == reload and not game_over:
            reloaded = True
            pygame.time.set_timer(reload, 0)

        if event.type == move_invaders_sideways and not game_over:
            if move_right:
                for invader in invaders: invader.move_ip(10,0)
                side_steps += 1
            else:
                for invader in invaders: invader.move_ip(-10,0)
                side_steps -= 1
            if side_steps == 6 or side_steps == -6:
                if vert_steps <= 31: # and not moved_down
                    pygame.time.set_timer(move_invaders_sideways, 0)
                    pygame.time.set_timer(move_invaders_down, MOVE_DOWN)
                # keep invaders moving horizontally after 31 down movements    
                else: move_right = not move_right

        if event.type == move_invaders_down and not game_over:
            #for i in range(20): print("down event")
            move_right = not move_right
            animate_invaders = True
            animation_time = ticks
            # reset move_sideways timer
            pygame.time.set_timer(move_invaders_sideways, MOVE_SIDEWAYS)
            # cancel move_down timer
            pygame.time.set_timer(move_invaders_down, 0)
            for invader in invaders: invader.move_ip(0,10)
            vert_steps += 1

        if event.type == bonus and not game_over:
            #a = Rect(769,20,45,15)
            bonus_invaders.append(Rect(797,20,45,15))

    # keyboard polling
    pressed = pygame.key.get_pressed()
    if pressed[K_ESCAPE]: pygame.quit(), sys.exit()
    elif pressed[K_RETURN]:
        if game_over: game_over = False
    elif pressed[K_d] or pressed[K_RIGHT]:player.move_ip((8, 0))
    #player_gun.move_ip((8,0))
    elif pressed[K_a] or pressed[K_LEFT]: player.move_ip((-8, 0))
    if pressed[K_SPACE]:
        if reloaded:
            reloaded = False
            # create timeout of RELOAD_SPEED
            pygame.time.set_timer(reload, RELOAD_SPEED)
            # shrink copy of player rect to imitate a missile
            missile = player.copy().inflate(-38, -10)
            # spawn missile higher to ensure appears missile fired from 'gun'
            # when the ship is moving horizontally
            missile.y -= 9
            shots.append(missile)
            #missile_sound.play()

    backbuffer.fill(BLACK)

    if not game_over:
        playing = True
        if level == 0:
            level = next_level(level)
            invaders, colors, initial_invaders = load_level(level)
            move_right, move_down, reloaded = True, True, True
            vert_steps = 0
            side_steps = 0
            moved_down = False
            invaders_paused = False
            pygame.time.set_timer(invader_shoot, 500)
            shoot_level = 1

        for shot in invader_shots:
            shot.move_ip((0,random.randint(5,11)))
            if not backbuffer.get_rect().contains(shot):
                i = invader_shots.index(shot)
                del invader_shots[i]
                del inv_shot_colors[i]
            if shot.colliderect(player):
                lives -= 1
                if lives < 0:
                    lives = 0
                    game_over = True
                i = invader_shots.index(shot)
                del invader_shots[i]
                del inv_shot_colors[i]

        for shot in shots:
            shot.move_ip((0, -8))
            for inv_shot in invader_shots:
                if inv_shot.colliderect(shot):
                    shots.remove(shot)
                    i = invader_shots.index(inv_shot)
                    del invader_shots[i]
                    del inv_shot_colors[i]
            for b_invader in bonus_invaders:
                if b_invader.colliderect(shot):
                    shots.remove(shot)
                    i = bonus_invaders.index(b_invader)
                    del bonus_invaders[i]
                    score += 1
            if not backbuffer.get_rect().contains(shot):
                shots.remove(shot)
            else:
                hit = False
                for invader in invaders:
                    if invader.colliderect(shot):
                        score += 1
                        hit = True
                        i = invaders.index(invader)
                        del invaders[i]
                        del colors[i]
                if hit: shots.remove(shot)

        # move bonus invader        
        for bonus_invader in bonus_invaders:
            bonus_invader.move_ip((-4,0 ))
##            if not screen.get_rect().contains(bonus_invader):
##                bonus_invaders.remove(bonus_invader)
            if bonus_invader.x < -55:
                bonus_invaders.remove(bonus_invader)

        # check if all invaders killed, if so, move to next level
        if len(invaders) == 0:
            level = next_level(level)
            invaders, colors, initial_invaders = load_level(level)
            move_right, move_down, reloaded = True, True, True
            vert_steps = 0
            side_steps = 0
            moved_down = False
            invaders_paused = False
            pygame.time.set_timer(invader_shoot, 500)
            shoot_level = 1

        # adjust shot freq when invader numbers decrease
        if len(invaders) < initial_invaders*.75 and shoot_level == 1:
            pygame.time.set_timer(invader_shoot, 750)
            shoot_level = 2
        elif len(invaders) < initial_invaders*.5 and shoot_level == 2:
            pygame.time.set_timer(invader_shoot, 1000)
            shoot_level = 3
        elif len(invaders) < initial_invaders*.25 and shoot_level == 3:
            pygame.time.set_timer(invader_shoot, 1500)
            shoot_level = 4

        # draw invaders        
        for rect, (a, b) in zip(invaders, colors):
            animate_invaders = draw_invader(backbuffer, rect, a, b,\
                                            animate_invaders, ticks, \
                                            animation_time)

        # draw bonus invaders
        if ticks > bonus_timer + 169:
                bonus_timer = ticks # change colors every 169ms approx
        for bonus_invader in bonus_invaders:
            pygame.draw.rect(backbuffer, (0,0,0,0), bonus_invader)
            pygame.draw.ellipse(backbuffer,DIMGRAY,bonus_invader)
            for i in range(6):
                bonus_color = next(bonus_colors)
                bx = next(bonus_x)
                draw_bonus_invader(i, bonus_color, bx, bonus_x)

        # draw space ship shots
        for shot in shots:
            pygame.draw.rect(backbuffer, (255,0,0), shot)
        # draw invader shots
        for shot, color in zip(invader_shots, inv_shot_colors):
            pygame.draw.rect(backbuffer, color, shot)

        #update 'gun' position and draw ship/gun
        #player_gun = Rect(player.x, player.y, 6, 4)
        player_gun.x = player.x+18
        pygame.draw.rect(backbuffer, DIMGRAY, player)
        pygame.draw.rect(backbuffer, DIMGRAY, player_gun)

        player.clamp_ip(backbuffer.get_rect())

        print_text(backbuffer, font1, "Invaders Pnwed: {}".format(score),\
                   the_screen, x=590, y=0)
        print_text(backbuffer, font1, "Lives: {}".format(lives), the_screen,\
                   x=0, y=0)
        print_text(backbuffer, font1, "Level: {}".format(level), the_screen,\
                   x=0, y=580)

    if game_over:
        if playing:
            game_is_over(backbuffer, font2, ticks)
            playing = False
            level = 0
            lives = 2
            score = 0
            shots, invader_shots, inv_shot_colors, bonus_invaders = [], [], [], []

        print_text(backbuffer, font2, "SPACE INVADERS", the_screen, y=5,\
                   center=True)
        draw_title_invader()

        if ticks > flash_timer + 800: # "press to play" flashing text
            insert = next(the_text)
            flash_timer = ticks
        print_text(backbuffer, font3, insert, the_screen, y =\
                   the_screen.height-40, center=True)

    screen.blit(backbuffer, (0,0))
    pygame.display.update()
    fpsclock.tick(30)
\$\endgroup\$
  • 1
    \$\begingroup\$ Am I mistaken or did you use the same code five times in draw_bonus_invader? \$\endgroup\$ – Zeta Sep 30 '16 at 16:22
  • \$\begingroup\$ Hehe, indeed. Thinks to self "noob". \$\endgroup\$ – Michael Johnson Oct 3 '16 at 10:44
  • \$\begingroup\$ I have rolled back the last edit. Please see what you may and may not do after receiving answers. \$\endgroup\$ – Vogel612 Oct 3 '16 at 15:38
6
\$\begingroup\$

Just a minor improvements.

1) If you count something manually or repeat exact lines one after other probably you do something wrong. Use iteration if possible

def draw_bonus_invader(i, bonus_color, bx, bonus_x):
    x, y = bonus_invader.x, bonus_invader.y
    pygame.draw.circle(backbuffer, bonus_color, (x+bx, y+7), 2)
    if i == 5:
        bx = next(bonus_x)

2) Try not to use \ to split logical line. You don’t need it anyway if splitting inside (). Also I'm not familiar with PyGame, so I can guess what the parameters are but not always:

# wrong
pygame.draw.line(backbuffer, (150, a, b),(rect.x+115, rect.y),\
                     (rect.x+50, rect.y-55),2 )

# better
x, y = rect.x, rect.y
pygame.draw.line(Surface=backbuffer,   color=(150, a, b), 
                 start_pos=(x+115, y), end_pos=(x+50, y-55), width=2)

3) # create user events and # event timers looks a bit painful. You should try to use dictionaries more often for creating special options and then just iterate over them.

Also I'd create list of strings ['bonus', 'invader_shoot', ...] and just use it to create dictionary

events_map = {'bonus':         (event, opt_1, opt_2), 
              'invader_shoot': (event, opt_1, opt_2),
              ...}

But that requires replacing many variables like this:

bonus -->     event = events_map['bonus'][0]              
all param --> event, opt_1, opt_2 = events_map['bonus'] 

Also note that I try to sort functions and variables in alphabetic order if possible.

# create user events

bonus                  = pygame.USEREVENT + 5
invader_shoot          = pygame.USEREVENT + 4
move_invaders_down     = pygame.USEREVENT + 2
move_invaders_sideways = pygame.USEREVENT + 1
reload                 = pygame.USEREVENT + 3

# event timers

options_map = {bonus:                  BONUS_FREQ,
               invader_shoot:          INV_SHOOT_FREQ,
               move_invaders_down:     0,
               move_invaders_sideways: MOVE_SIDEWAYS,
               reload:                 RELOAD_SPEED
               }
for (event, option) in event_map.items():
    pygame.time.set_timer(event, option)

4) Multiple if statements are bad, since they all will be processed one after another every time code is executed. Use elif or better: dictionaries. Again:

# Assigned functions aren’t executed. You just assign names (if without ())
event_map = {bonus: bonus_foo, invader_shoot: invader_shoot_foo,
             KEYUP: KEYUP_foo, reload: reload_foo,  ...}

# event loop
while True:
    ticks = pygame.time.get_ticks()
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        elif not game_over:
            # Here is where execution begins
            event_map[event.type]()

5) Try to organise code in general: - Defined functions are in random order. Use alphabetic. It will be easier to find and edit each. - Constants are everywhere. Constants block are in non-clear order. And they are somewhat mixed with code. Define them at the place they are needed if possible. If used in many situations, place somewhere at the beginning or use separate module just for the constants and functions.

Edit:

If you don’t want to introduce classes you should probably stick with elif statements. You can try to modify global variables, but very carefully since they can mess code logic easily and usually this a bad practice to use many global variables:

reloaded = False

def reload_f():
    global reloaded
    reloaded = True        

print(reloaded) #--> False
reload_f()
print(reloaded) #--> True

On the other hand with classes you can edit class parameters anywhere in the code as well as obtain parameter values:

class MyShip(object):

    def __init__(self):
        object.__init__(self)
        self.isReloaded = False
        self.bullets = 0

    def reload(self):
        self.isReloaded = True
        self.bullets = 10


player = MyShip()
print(player.isReloaded, player.bullets) #--> False, 0

bullets = player.reload()
print(player.isReloaded, player.bullets) #-->True, 10

So instead of elif statement:

# define some classes with useful methods
player = MyShip()
invaders = InvaderShips()

for event in pygame.event.get():
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    elif not game_over:
        event_map = {reload: player.reload, 
                     move_invaders_sideways: invaders.move_sideways,
                     ...}

        if event.type in event_map:
            event_map[event.type]
\$\endgroup\$
  • \$\begingroup\$ Thank you for taking the time to highlight that. Much appreciated! I've implemented your proposed amendments with the exception the of the draw functions(they don't take keyword args) and the event map in "4". Unless I haven't grasped fully what you were suggesting, the closest I could come up has been added to my original post as an edit. I had to return values from the functions and this can't be done with the dict method (I think) Thanks again, Dave. \$\endgroup\$ – Michael Johnson Oct 3 '16 at 15:29
  • \$\begingroup\$ The code is now in an answer below Vasyalisk. \$\endgroup\$ – Michael Johnson Oct 3 '16 at 15:44
  • \$\begingroup\$ Added new info at the end of the post if you would like to turn into OOE. Otherwise you should probably use elif logic. Good luck! \$\endgroup\$ – Vasyalisk Oct 3 '16 at 20:01
  • \$\begingroup\$ Thanks for the edit. I might well convert the game to use classes once I've finished it. That would probably be a good learning exercise. Using the existing structure was interesting but I wouldn't want to use it again for something like this. \$\endgroup\$ – Michael Johnson Oct 4 '16 at 7:13
1
\$\begingroup\$

Reworked event loop. Tidies up and corrects the original somewhat:

for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        elif not game_over:
            if event.type == invader_shoot:
                invader_shoot_f()
            elif event.type == reload:
                reloaded = reload_f()
            elif event.type == move_invaders_sideways:
               move_right, side_steps = move_invaders_sideways_f(move_right,
                                                                 side_steps,
                                                                 vert_steps)
            elif event.type == move_invaders_down:
                move_right, animate_invaders, animation_time, vert_steps = \
                            move_invaders_down_f(move_right, vert_steps, ticks)
            elif event.type == bonus:
                bonus_invaders.append(Rect(797, 20, 45, 15))  

There was also a bug present which would arise whenever there was an invader shot - player collision and that invader shot was also partially off the screen:

if shot.colliderect(player):

changed to:

# ignore any collisions with invader shot partially off screen
# Let these be picked up by the preceding few lines of code only
# 10 is the height of the invader shots

if shot.colliderect(player) and shot.y < the_screen.height -10:
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.