14
\$\begingroup\$

This is my first game made using Pygame of about 550 lines of code. Advice and suggestions are more than welcome.

## Game music Attribution
##Frozen Jam by tgfcoder <https://twitter.com/tgfcoder> licensed under CC-BY-3 <http://creativecommons.org/licenses/by/3.0/>
from __future__ import division
import pygame
import random
from os import path
## assets folder
img_dir = path.join(path.dirname(__file__), 'assets')
sound_folder = path.join(path.dirname(__file__), 'sounds')
###############################
## to be placed in "constant.py" later
WIDTH = 480
HEIGHT = 600
FPS = 60
POWERUP_TIME = 5000
# Define Colors 
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
###############################
###############################
## to placed in "__init__.py" later
## initialize pygame and create window
pygame.init()
pygame.mixer.init() ## For sound
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Space Shooter")
clock = pygame.time.Clock() ## For syncing the FPS
###############################
font_name = pygame.font.match_font('arial')
def main_menu():
 global screen
 menu_song = pygame.mixer.music.load(path.join(sound_folder, "menu.ogg"))
 pygame.mixer.music.play(-1)
 title = pygame.image.load(path.join(img_dir, "main.png")).convert()
 title = pygame.transform.scale(title, (WIDTH, HEIGHT), screen)
 screen.blit(title, (0,0))
 pygame.display.update()
 while True:
 ev = pygame.event.poll()
 if ev.type == pygame.KEYDOWN:
 if ev.key == pygame.K_RETURN:
 break
 elif ev.key == pygame.K_q:
 pygame.quit()
 quit()
 else:
 draw_text(screen, "Press [ENTER] To Begin", 30, WIDTH/2, HEIGHT/2)
 draw_text(screen, "or [Q] To Quit", 30, WIDTH/2, (HEIGHT/2)+40)
 pygame.display.update()
 #pygame.mixer.music.stop()
 ready = pygame.mixer.Sound(path.join(sound_folder,'getready.ogg'))
 ready.play()
 screen.fill((0,0,0))
 draw_text(screen, "GET READY!", 40, WIDTH/2, HEIGHT/2)
 pygame.display.update()
def draw_text(surf, text, size, x, y):
 ## selecting a cross platform font to display the score
 font = pygame.font.Font(font_name, size)
 text_surface = font.render(text, True, WHITE) ## True denotes the font to be anti-aliased 
 text_rect = text_surface.get_rect()
 text_rect.midtop = (x, y)
 surf.blit(text_surface, text_rect)
def draw_shield_bar(surf, x, y, pct):
 if pct < 0:
 pct = 0 
 BAR_LENGTH = 100
 BAR_HEIGHT = 10
 fill = (pct / 100) * BAR_LENGTH
 outline_rect = pygame.Rect(x, y, BAR_LENGTH, BAR_HEIGHT)
 fill_rect = pygame.Rect(x, y, fill, BAR_HEIGHT)
 pygame.draw.rect(surf, GREEN, fill_rect)
 pygame.draw.rect(surf, WHITE, outline_rect, 2)
def draw_lives(surf, x, y, lives, img):
 for i in range(lives):
 img_rect= img.get_rect()
 img_rect.x = x + 30 * i
 img_rect.y = y
 surf.blit(img, img_rect)
def newmob():
 mob_element = Mob()
 all_sprites.add(mob_element)
 mobs.add(mob_element)
class Explosion(pygame.sprite.Sprite):
 def __init__(self, center, size):
 pygame.sprite.Sprite.__init__(self)
 self.size = size
 self.image = explosion_anim[self.size][0]
 self.rect = self.image.get_rect()
 self.rect.center = center
 self.frame = 0 
 self.last_update = pygame.time.get_ticks()
 self.frame_rate = 75
 def update(self):
 now = pygame.time.get_ticks()
 if now - self.last_update > self.frame_rate:
 self.last_update = now
 self.frame += 1
 if self.frame == len(explosion_anim[self.size]):
 self.kill()
 else:
 center = self.rect.center
 self.image = explosion_anim[self.size][self.frame]
 self.rect = self.image.get_rect()
 self.rect.center = center
class Player(pygame.sprite.Sprite):
 def __init__(self):
 pygame.sprite.Sprite.__init__(self)
 ## scale the player img down
 self.image = pygame.transform.scale(player_img, (50, 38))
 self.image.set_colorkey(BLACK)
 self.rect = self.image.get_rect()
 self.radius = 20
 self.rect.centerx = WIDTH / 2
 self.rect.bottom = HEIGHT - 10
 self.speedx = 0 
 self.shield = 100
 self.shoot_delay = 250
 self.last_shot = pygame.time.get_ticks()
 self.lives = 3
 self.hidden = False
 self.hide_timer = pygame.time.get_ticks()
 self.power = 1
 self.power_timer = pygame.time.get_ticks()
 def update(self):
 ## time out for powerups
 if self.power >=2 and pygame.time.get_ticks() - self.power_time > POWERUP_TIME:
 self.power -= 1
 self.power_time = pygame.time.get_ticks()
 ## unhide 
 if self.hidden and pygame.time.get_ticks() - self.hide_timer > 1000:
 self.hidden = False
 self.rect.centerx = WIDTH / 2
 self.rect.bottom = HEIGHT - 30
 self.speedx = 0 ## makes the player static in the screen by default. 
 # then we have to check whether there is an event hanlding being done for the arrow keys being 
 ## pressed 
 ## will give back a list of the keys which happen to be pressed down at that moment
 keystate = pygame.key.get_pressed() 
 if keystate[pygame.K_LEFT]:
 self.speedx = -5
 elif keystate[pygame.K_RIGHT]:
 self.speedx = 5
 #Fire weapons by holding spacebar
 if keystate[pygame.K_SPACE]:
 self.shoot()
 ## check for the borders at the left and right
 if self.rect.right > WIDTH:
 self.rect.right = WIDTH
 if self.rect.left < 0:
 self.rect.left = 0
 self.rect.x += self.speedx
 def shoot(self):
 ## to tell the bullet where to spawn
 now = pygame.time.get_ticks()
 if now - self.last_shot > self.shoot_delay:
 self.last_shot = now
 if self.power == 1:
 bullet = Bullet(self.rect.centerx, self.rect.top)
 all_sprites.add(bullet)
 bullets.add(bullet)
 shooting_sound.play()
 if self.power == 2:
 bullet1 = Bullet(self.rect.left, self.rect.centery)
 bullet2 = Bullet(self.rect.right, self.rect.centery)
 all_sprites.add(bullet1)
 all_sprites.add(bullet2)
 bullets.add(bullet1)
 bullets.add(bullet2)
 shooting_sound.play()
 """ MOAR POWAH """
 if self.power >= 3:
 bullet1 = Bullet(self.rect.left, self.rect.centery)
 bullet2 = Bullet(self.rect.right, self.rect.centery)
 missile1 = Missile(self.rect.centerx, self.rect.top) # Missile shoots from center of ship
 all_sprites.add(bullet1)
 all_sprites.add(bullet2)
 all_sprites.add(missile1)
 bullets.add(bullet1)
 bullets.add(bullet2)
 bullets.add(missile1)
 shooting_sound.play()
 missile_sound.play()
 def powerup(self):
 self.power += 1
 self.power_time = pygame.time.get_ticks()
 def hide(self):
 self.hidden = True
 self.hide_timer = pygame.time.get_ticks()
 self.rect.center = (WIDTH / 2, HEIGHT + 200)
# defines the enemies
class Mob(pygame.sprite.Sprite):
 def __init__(self):
 pygame.sprite.Sprite.__init__(self)
 self.image_orig = random.choice(meteor_images)
 self.image_orig.set_colorkey(BLACK)
 self.image = self.image_orig.copy()
 self.rect = self.image.get_rect()
 self.radius = int(self.rect.width *.90 / 2)
 self.rect.x = random.randrange(0, WIDTH - self.rect.width)
 self.rect.y = random.randrange(-150, -100)
 self.speedy = random.randrange(5, 20) ## for randomizing the speed of the Mob
 ## randomize the movements a little more 
 self.speedx = random.randrange(-3, 3)
 ## adding rotation to the mob element
 self.rotation = 0
 self.rotation_speed = random.randrange(-8, 8)
 self.last_update = pygame.time.get_ticks() ## time when the rotation has to happen
 def rotate(self):
 time_now = pygame.time.get_ticks()
 if time_now - self.last_update > 50: # in milliseconds
 self.last_update = time_now
 self.rotation = (self.rotation + self.rotation_speed) % 360 
 new_image = pygame.transform.rotate(self.image_orig, self.rotation)
 old_center = self.rect.center
 self.image = new_image
 self.rect = self.image.get_rect()
 self.rect.center = old_center
 def update(self):
 self.rotate()
 self.rect.x += self.speedx
 self.rect.y += self.speedy
 ## now what if the mob element goes out of the screen
 if (self.rect.top > HEIGHT + 10) or (self.rect.left < -25) or (self.rect.right > WIDTH + 20):
 self.rect.x = random.randrange(0, WIDTH - self.rect.width)
 self.rect.y = random.randrange(-100, -40)
 self.speedy = random.randrange(1, 8) ## for randomizing the speed of the Mob
## defines the sprite for Powerups
class Pow(pygame.sprite.Sprite):
 def __init__(self, center):
 pygame.sprite.Sprite.__init__(self)
 self.type = random.choice(['shield', 'gun'])
 self.image = powerup_images[self.type]
 self.image.set_colorkey(BLACK)
 self.rect = self.image.get_rect()
 ## place the bullet according to the current position of the player
 self.rect.center = center
 self.speedy = 2
 def update(self):
 """should spawn right in front of the player"""
 self.rect.y += self.speedy
 ## kill the sprite after it moves over the top border
 if self.rect.top > HEIGHT:
 self.kill()
## defines the sprite for bullets
class Bullet(pygame.sprite.Sprite):
 def __init__(self, x, y):
 pygame.sprite.Sprite.__init__(self)
 self.image = bullet_img
 self.image.set_colorkey(BLACK)
 self.rect = self.image.get_rect()
 ## place the bullet according to the current position of the player
 self.rect.bottom = y 
 self.rect.centerx = x
 self.speedy = -10
 def update(self):
 """should spawn right in front of the player"""
 self.rect.y += self.speedy
 ## kill the sprite after it moves over the top border
 if self.rect.bottom < 0:
 self.kill()
 ## now we need a way to shoot
 ## lets bind it to "spacebar".
 ## adding an event for it in Game loop
## FIRE ZE MISSILES
class Missile(pygame.sprite.Sprite):
 def __init__(self, x, y):
 pygame.sprite.Sprite.__init__(self)
 self.image = missile_img
 self.image.set_colorkey(BLACK)
 self.rect = self.image.get_rect()
 self.rect.bottom = y
 self.rect.centerx = x
 self.speedy = -10
 def update(self):
 """should spawn right in front of the player"""
 self.rect.y += self.speedy
 if self.rect.bottom < 0:
 self.kill()
###################################################
## Load all game images
background = pygame.image.load(path.join(img_dir, 'starfield.png')).convert()
background_rect = background.get_rect()
## ^^ draw this rect first 
player_img = pygame.image.load(path.join(img_dir, 'playerShip1_orange.png')).convert()
player_mini_img = pygame.transform.scale(player_img, (25, 19))
player_mini_img.set_colorkey(BLACK)
bullet_img = pygame.image.load(path.join(img_dir, 'laserRed16.png')).convert()
missile_img = pygame.image.load(path.join(img_dir, 'missile.png')).convert_alpha()
# meteor_img = pygame.image.load(path.join(img_dir, 'meteorBrown_med1.png')).convert()
meteor_images = []
meteor_list = [
 'meteorBrown_big1.png',
 'meteorBrown_big2.png', 
 'meteorBrown_med1.png', 
 'meteorBrown_med3.png',
 'meteorBrown_small1.png',
 'meteorBrown_small2.png',
 'meteorBrown_tiny1.png'
]
for image in meteor_list:
 meteor_images.append(pygame.image.load(path.join(img_dir, image)).convert())
## meteor explosion
explosion_anim = {}
explosion_anim['lg'] = []
explosion_anim['sm'] = []
explosion_anim['player'] = []
for i in range(9):
 filename = 'regularExplosion0{}.png'.format(i)
 img = pygame.image.load(path.join(img_dir, filename)).convert()
 img.set_colorkey(BLACK)
 ## resize the explosion
 img_lg = pygame.transform.scale(img, (75, 75))
 explosion_anim['lg'].append(img_lg)
 img_sm = pygame.transform.scale(img, (32, 32))
 explosion_anim['sm'].append(img_sm)
 ## player explosion
 filename = 'sonicExplosion0{}.png'.format(i)
 img = pygame.image.load(path.join(img_dir, filename)).convert()
 img.set_colorkey(BLACK)
 explosion_anim['player'].append(img)
## load power ups
powerup_images = {}
powerup_images['shield'] = pygame.image.load(path.join(img_dir, 'shield_gold.png')).convert()
powerup_images['gun'] = pygame.image.load(path.join(img_dir, 'bolt_gold.png')).convert()
###################################################
###################################################
### Load all game sounds
shooting_sound = pygame.mixer.Sound(path.join(sound_folder, 'pew.wav'))
missile_sound = pygame.mixer.Sound(path.join(sound_folder, 'rocket.ogg'))
expl_sounds = []
for sound in ['expl3.wav', 'expl6.wav']:
 expl_sounds.append(pygame.mixer.Sound(path.join(sound_folder, sound)))
## main background music
#pygame.mixer.music.load(path.join(sound_folder, 'tgfcoder-FrozenJam-SeamlessLoop.ogg'))
pygame.mixer.music.set_volume(0.2) ## simmered the sound down a little
player_die_sound = pygame.mixer.Sound(path.join(sound_folder, 'rumble1.ogg'))
###################################################
## group all the sprites together for ease of update
all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)
## spawn a group of mob
mobs = pygame.sprite.Group()
for i in range(8): ## 8 mobs
 # mob_element = Mob()
 # all_sprites.add(mob_element)
 # mobs.add(mob_element)
 newmob()
## group for bullets
bullets = pygame.sprite.Group()
powerups = pygame.sprite.Group()
#### Score board variable
score = 0
## TODO: make the game music loop over again and again. play(loops=-1) is not working
# Error : 
# TypeError: play() takes no keyword arguments
#pygame.mixer.music.play()
#############################
## Game loop
running = True
menu_display = True
while running:
 if menu_display:
 main_menu()
 pygame.time.wait(3000)
 #Stop menu music
 pygame.mixer.music.stop()
 #Play the gameplay music
 pygame.mixer.music.load(path.join(sound_folder, 'tgfcoder-FrozenJam-SeamlessLoop.ogg'))
 pygame.mixer.music.play(-1) ## makes the gameplay sound in an endless loop
 menu_display = False
 #1 Process input/events
 clock.tick(FPS) ## will make the loop run at the same speed all the time
 for event in pygame.event.get(): # gets all the events which have occured till now and keeps tab of them.
 ## listening for the the X button at the top
 if event.type == pygame.QUIT:
 running = False
 ## Press ESC to exit game
 if event.type == pygame.KEYDOWN:
 if event.key == pygame.K_ESCAPE:
 running = False
 # ## event for shooting the bullets
 # elif event.type == pygame.KEYDOWN:
 # if event.key == pygame.K_SPACE:
 # player.shoot() ## we have to define the shoot() function
 #2 Update
 all_sprites.update()
 ## check if a bullet hit a mob
 ## now we have a group of bullets and a group of mob
 hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
 ## now as we delete the mob element when we hit one with a bullet, we need to respawn them again
 ## as there will be no mob_elements left out 
 for hit in hits:
 score += 50 - hit.radius ## give different scores for hitting big and small metoers
 random.choice(expl_sounds).play()
 # m = Mob()
 # all_sprites.add(m)
 # mobs.add(m)
 expl = Explosion(hit.rect.center, 'lg')
 all_sprites.add(expl)
 if random.random() > 0.9:
 pow = Pow(hit.rect.center)
 all_sprites.add(pow)
 powerups.add(pow)
 newmob() ## spawn a new mob
 ## ^^ the above loop will create the amount of mob objects which were killed spawn again
 #########################
 ## check if the player collides with the mob
 hits = pygame.sprite.spritecollide(player, mobs, True, pygame.sprite.collide_circle) ## gives back a list, True makes the mob element disappear
 for hit in hits:
 player.shield -= hit.radius * 2
 expl = Explosion(hit.rect.center, 'sm')
 all_sprites.add(expl)
 newmob()
 if player.shield <= 0: 
 player_die_sound.play()
 death_explosion = Explosion(player.rect.center, 'player')
 all_sprites.add(death_explosion)
 # running = False ## GAME OVER 3:D
 player.hide()
 player.lives -= 1
 player.shield = 100
 ## if the player hit a power up
 hits = pygame.sprite.spritecollide(player, powerups, True)
 for hit in hits:
 if hit.type == 'shield':
 player.shield += random.randrange(10, 30)
 if player.shield >= 100:
 player.shield = 100
 if hit.type == 'gun':
 player.powerup()
 ## if player died and the explosion has finished, end game
 if player.lives == 0 and not death_explosion.alive():
 running = False
 # menu_display = True
 # pygame.display.update()
 #3 Draw/render
 screen.fill(BLACK)
 ## draw the stargaze.png image
 screen.blit(background, background_rect)
 all_sprites.draw(screen)
 draw_text(screen, str(score), 18, WIDTH / 2, 10) ## 10px down from the screen
 draw_shield_bar(screen, 5, 5, player.shield)
 # Draw lives
 draw_lives(screen, WIDTH - 100, 5, player.lives, player_mini_img)
 ## Done after drawing everything to the screen
 pygame.display.flip() 
pygame.quit()
asked Jan 25, 2016 at 19:19
\$\endgroup\$
2
  • \$\begingroup\$ To help reviewers give you better answers, please add sufficient context to your question. The more you tell us about what your code does and what the purpose of doing that is, the easier it will be for reviewers to help you. See also this meta question. \$\endgroup\$ Commented Jan 26, 2016 at 11:57
  • \$\begingroup\$ Will keep that in mind @SuperBiasedMan \$\endgroup\$ Commented Jan 26, 2016 at 15:46

1 Answer 1

5
\$\begingroup\$

You're following good style conventions, that's a good sign. A lot of people don't do this consistently. (Note I'm not sure if you're a beginner, you said this was your first game but that was all). You do occasionally let lines run too long or have the wrong spacing. It's best if you read the PEP0008, Python's official style guide. I have some general notes from reading through your code, but I've definitely missed things to be improved. On pep8online you can post your whole code in and get a breakdown of every part of your code that it finds in violation of PEP rules. It's not perfect, but will reveal some good rules for you to follow with tons of examples. I still recommend reading PEP0008 when you can.

You lay out colour constants at the start, but then you don't always use them. For instance, in main_menu, why use

screen.fill((0,0,0))

and not

screen.fill(BLACK)

At the risk of stating the obvious, constants are better to use as they're more readable to understand, and easier to change. If you decide that you want to adjust your colour palette to make the game look blue tinted, you could do that easily if all colours are set by your constant values.

In draw_shield_bar you use an if statement to ensure that pct is always 0 at the lowest. An alternative is to use max, which will return the maximum of values passed to it. ie. if you pass max(pct, 0) then it returns pct if it's 0 or higher, but otherwise will return 0. Your mileage may vary on which is more readable but I prefer:

pct = max(pct, 0)

I presume pct is percent, but it's not a clear name. Try to avoid such shortened names, they usually just add confusion. A clear full word makes your code a lot more readable, and shaving those characters doesn't really get you much.

Also you declare BAR_HEIGHT and BAR_LENGTH locally in the function but they're named as if they're constants, with all uppercase letters. You should either make them constants at the top level, alongside your other constants like the colours that will eventually be in constants.py or just declare them as normal values that happen to always be the same. ie. length = 100. I suggest the former, as long as you can name them clearly.

In draw_lives, you calculate img_rect.x by using 30. But since 30 isn't a variable, it's not at all clear what it corresponds to. This is known as a magic number, because the reason behind it is unknown to the reader. These should be avoided, instead you should have a named constant like LIFE_SPACING or something more appropriate so that it's much easier to follow what's happening.

Still on constants, you may like to use something called a Class constant. When you declare a class, you usually set its attributes in __init__, but you can also set constant attributes above __init__, in the class definition. Like this:

class Explosion(pygame.sprite.Sprite):
 FRAME_RATE = 75
 def __init__(self, center, size):
 ...

This makes it easier to see that FRAME_RATE is constant across all Explosions. It can still be accessed with self.Explosion, and can be additionally accessed with Explosion.FRAME_RATE, which can be useful when you want to get the value from outside a class instance context.

answered Jan 26, 2016 at 12:21
\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the suggestions. Will keep them mind. I checked out the PEP0008 link. But do you have something which summarizes it properly without losing out the key points. Or else I will have to fall back to that. \$\endgroup\$ Commented Jan 26, 2016 at 16:02
  • \$\begingroup\$ @prodicus I was going to add some specifics to my answer, but I think it's more useful to link to pep8online, where you can post your whole code in and get a breakdown of every part of your code that it can find in violation of PEP rules. It's not perfect, but will reveal some good rules for you to follow with tons of examples. \$\endgroup\$ Commented Jan 26, 2016 at 17:26
  • \$\begingroup\$ Also have a look at autopep8 which automatically applies pep8 formatting \$\endgroup\$ Commented Dec 3, 2019 at 5:22

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.