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()
-
\$\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\$SuperBiasedMan– SuperBiasedMan2016年01月26日 11:57:20 +00:00Commented Jan 26, 2016 at 11:57
-
\$\begingroup\$ Will keep that in mind @SuperBiasedMan \$\endgroup\$Tasdik Rahman– Tasdik Rahman2016年01月26日 15:46:53 +00:00Commented Jan 26, 2016 at 15:46
1 Answer 1
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 Explosion
s. 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.
-
\$\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\$Tasdik Rahman– Tasdik Rahman2016年01月26日 16:02:21 +00:00Commented 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\$SuperBiasedMan– SuperBiasedMan2016年01月26日 17:26:48 +00:00Commented Jan 26, 2016 at 17:26
-
\$\begingroup\$ Also have a look at
autopep8
which automatically appliespep8
formatting \$\endgroup\$alexyorke– alexyorke2019年12月03日 05:22:14 +00:00Commented Dec 3, 2019 at 5:22