396 lines
12 KiB
Python
396 lines
12 KiB
Python
|
#!/usr/bin/env python
|
||
|
""" pygame.examples.aliens
|
||
|
|
||
|
Shows a mini game where you have to defend against aliens.
|
||
|
|
||
|
What does it show you about pygame?
|
||
|
|
||
|
* pg.sprite, the difference between Sprite and Group.
|
||
|
* dirty rectangle optimization for processing for speed.
|
||
|
* music with pg.mixer.music, including fadeout
|
||
|
* sound effects with pg.Sound
|
||
|
* event processing, keyboard handling, QUIT handling.
|
||
|
* a main loop frame limited with a game clock from pg.time.Clock
|
||
|
* fullscreen switching.
|
||
|
|
||
|
|
||
|
Controls
|
||
|
--------
|
||
|
|
||
|
* Left and right arrows to move.
|
||
|
* Space bar to shoot
|
||
|
* f key to toggle between fullscreen.
|
||
|
|
||
|
"""
|
||
|
|
||
|
import os
|
||
|
import random
|
||
|
from typing import List
|
||
|
|
||
|
# import basic pygame modules
|
||
|
import pygame as pg
|
||
|
|
||
|
# see if we can load more than standard BMP
|
||
|
if not pg.image.get_extended():
|
||
|
raise SystemExit("Sorry, extended image module required")
|
||
|
|
||
|
|
||
|
# game constants
|
||
|
MAX_SHOTS = 2 # most player bullets onscreen
|
||
|
ALIEN_ODDS = 22 # chances a new alien appears
|
||
|
BOMB_ODDS = 60 # chances a new bomb will drop
|
||
|
ALIEN_RELOAD = 12 # frames between new aliens
|
||
|
SCREENRECT = pg.Rect(0, 0, 640, 480)
|
||
|
SCORE = 0
|
||
|
|
||
|
main_dir = os.path.split(os.path.abspath(__file__))[0]
|
||
|
|
||
|
|
||
|
def load_image(file):
|
||
|
"""loads an image, prepares it for play"""
|
||
|
file = os.path.join(main_dir, "data", file)
|
||
|
try:
|
||
|
surface = pg.image.load(file)
|
||
|
except pg.error:
|
||
|
raise SystemExit(f'Could not load image "{file}" {pg.get_error()}')
|
||
|
return surface.convert()
|
||
|
|
||
|
|
||
|
def load_sound(file):
|
||
|
"""because pygame can be compiled without mixer."""
|
||
|
if not pg.mixer:
|
||
|
return None
|
||
|
file = os.path.join(main_dir, "data", file)
|
||
|
try:
|
||
|
sound = pg.mixer.Sound(file)
|
||
|
return sound
|
||
|
except pg.error:
|
||
|
print(f"Warning, unable to load, {file}")
|
||
|
return None
|
||
|
|
||
|
|
||
|
# Each type of game object gets an init and an update function.
|
||
|
# The update function is called once per frame, and it is when each object should
|
||
|
# change its current position and state.
|
||
|
#
|
||
|
# The Player object actually gets a "move" function instead of update,
|
||
|
# since it is passed extra information about the keyboard.
|
||
|
|
||
|
|
||
|
class Player(pg.sprite.Sprite):
|
||
|
"""Representing the player as a moon buggy type car."""
|
||
|
|
||
|
speed = 10
|
||
|
bounce = 24
|
||
|
gun_offset = -11
|
||
|
images: List[pg.Surface] = []
|
||
|
|
||
|
def __init__(self, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.image = self.images[0]
|
||
|
self.rect = self.image.get_rect(midbottom=SCREENRECT.midbottom)
|
||
|
self.reloading = 0
|
||
|
self.origtop = self.rect.top
|
||
|
self.facing = -1
|
||
|
|
||
|
def move(self, direction):
|
||
|
if direction:
|
||
|
self.facing = direction
|
||
|
self.rect.move_ip(direction * self.speed, 0)
|
||
|
self.rect = self.rect.clamp(SCREENRECT)
|
||
|
if direction < 0:
|
||
|
self.image = self.images[0]
|
||
|
elif direction > 0:
|
||
|
self.image = self.images[1]
|
||
|
self.rect.top = self.origtop - (self.rect.left // self.bounce % 2)
|
||
|
|
||
|
def gunpos(self):
|
||
|
pos = self.facing * self.gun_offset + self.rect.centerx
|
||
|
return pos, self.rect.top
|
||
|
|
||
|
|
||
|
class Alien(pg.sprite.Sprite):
|
||
|
"""An alien space ship. That slowly moves down the screen."""
|
||
|
|
||
|
speed = 13
|
||
|
animcycle = 12
|
||
|
images: List[pg.Surface] = []
|
||
|
|
||
|
def __init__(self, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.image = self.images[0]
|
||
|
self.rect = self.image.get_rect()
|
||
|
self.facing = random.choice((-1, 1)) * Alien.speed
|
||
|
self.frame = 0
|
||
|
if self.facing < 0:
|
||
|
self.rect.right = SCREENRECT.right
|
||
|
|
||
|
def update(self):
|
||
|
self.rect.move_ip(self.facing, 0)
|
||
|
if not SCREENRECT.contains(self.rect):
|
||
|
self.facing = -self.facing
|
||
|
self.rect.top = self.rect.bottom + 1
|
||
|
self.rect = self.rect.clamp(SCREENRECT)
|
||
|
self.frame = self.frame + 1
|
||
|
self.image = self.images[self.frame // self.animcycle % 3]
|
||
|
|
||
|
|
||
|
class Explosion(pg.sprite.Sprite):
|
||
|
"""An explosion. Hopefully the Alien and not the player!"""
|
||
|
|
||
|
defaultlife = 12
|
||
|
animcycle = 3
|
||
|
images: List[pg.Surface] = []
|
||
|
|
||
|
def __init__(self, actor, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.image = self.images[0]
|
||
|
self.rect = self.image.get_rect(center=actor.rect.center)
|
||
|
self.life = self.defaultlife
|
||
|
|
||
|
def update(self):
|
||
|
"""called every time around the game loop.
|
||
|
|
||
|
Show the explosion surface for 'defaultlife'.
|
||
|
Every game tick(update), we decrease the 'life'.
|
||
|
|
||
|
Also we animate the explosion.
|
||
|
"""
|
||
|
self.life = self.life - 1
|
||
|
self.image = self.images[self.life // self.animcycle % 2]
|
||
|
if self.life <= 0:
|
||
|
self.kill()
|
||
|
|
||
|
|
||
|
class Shot(pg.sprite.Sprite):
|
||
|
"""a bullet the Player sprite fires."""
|
||
|
|
||
|
speed = -11
|
||
|
images: List[pg.Surface] = []
|
||
|
|
||
|
def __init__(self, pos, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.image = self.images[0]
|
||
|
self.rect = self.image.get_rect(midbottom=pos)
|
||
|
|
||
|
def update(self):
|
||
|
"""called every time around the game loop.
|
||
|
|
||
|
Every tick we move the shot upwards.
|
||
|
"""
|
||
|
self.rect.move_ip(0, self.speed)
|
||
|
if self.rect.top <= 0:
|
||
|
self.kill()
|
||
|
|
||
|
|
||
|
class Bomb(pg.sprite.Sprite):
|
||
|
"""A bomb the aliens drop."""
|
||
|
|
||
|
speed = 9
|
||
|
images: List[pg.Surface] = []
|
||
|
|
||
|
def __init__(self, alien, explosion_group, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.image = self.images[0]
|
||
|
self.rect = self.image.get_rect(midbottom=alien.rect.move(0, 5).midbottom)
|
||
|
self.explosion_group = explosion_group
|
||
|
|
||
|
def update(self):
|
||
|
"""called every time around the game loop.
|
||
|
|
||
|
Every frame we move the sprite 'rect' down.
|
||
|
When it reaches the bottom we:
|
||
|
|
||
|
- make an explosion.
|
||
|
- remove the Bomb.
|
||
|
"""
|
||
|
self.rect.move_ip(0, self.speed)
|
||
|
if self.rect.bottom >= 470:
|
||
|
Explosion(self, self.explosion_group)
|
||
|
self.kill()
|
||
|
|
||
|
|
||
|
class Score(pg.sprite.Sprite):
|
||
|
"""to keep track of the score."""
|
||
|
|
||
|
def __init__(self, *groups):
|
||
|
pg.sprite.Sprite.__init__(self, *groups)
|
||
|
self.font = pg.font.Font(None, 20)
|
||
|
self.font.set_italic(1)
|
||
|
self.color = "white"
|
||
|
self.lastscore = -1
|
||
|
self.update()
|
||
|
self.rect = self.image.get_rect().move(10, 450)
|
||
|
|
||
|
def update(self):
|
||
|
"""We only update the score in update() when it has changed."""
|
||
|
if SCORE != self.lastscore:
|
||
|
self.lastscore = SCORE
|
||
|
msg = f"Score: {SCORE}"
|
||
|
self.image = self.font.render(msg, 0, self.color)
|
||
|
|
||
|
|
||
|
def main(winstyle=0):
|
||
|
# Initialize pygame
|
||
|
if pg.get_sdl_version()[0] == 2:
|
||
|
pg.mixer.pre_init(44100, 32, 2, 1024)
|
||
|
pg.init()
|
||
|
if pg.mixer and not pg.mixer.get_init():
|
||
|
print("Warning, no sound")
|
||
|
pg.mixer = None
|
||
|
|
||
|
fullscreen = False
|
||
|
# Set the display mode
|
||
|
winstyle = 0 # |FULLSCREEN
|
||
|
bestdepth = pg.display.mode_ok(SCREENRECT.size, winstyle, 32)
|
||
|
screen = pg.display.set_mode(SCREENRECT.size, winstyle, bestdepth)
|
||
|
|
||
|
# Load images, assign to sprite classes
|
||
|
# (do this before the classes are used, after screen setup)
|
||
|
img = load_image("player1.gif")
|
||
|
Player.images = [img, pg.transform.flip(img, 1, 0)]
|
||
|
img = load_image("explosion1.gif")
|
||
|
Explosion.images = [img, pg.transform.flip(img, 1, 1)]
|
||
|
Alien.images = [load_image(im) for im in ("alien1.gif", "alien2.gif", "alien3.gif")]
|
||
|
Bomb.images = [load_image("bomb.gif")]
|
||
|
Shot.images = [load_image("shot.gif")]
|
||
|
|
||
|
# decorate the game window
|
||
|
icon = pg.transform.scale(Alien.images[0], (32, 32))
|
||
|
pg.display.set_icon(icon)
|
||
|
pg.display.set_caption("Pygame Aliens")
|
||
|
pg.mouse.set_visible(0)
|
||
|
|
||
|
# create the background, tile the bgd image
|
||
|
bgdtile = load_image("background.gif")
|
||
|
background = pg.Surface(SCREENRECT.size)
|
||
|
for x in range(0, SCREENRECT.width, bgdtile.get_width()):
|
||
|
background.blit(bgdtile, (x, 0))
|
||
|
screen.blit(background, (0, 0))
|
||
|
pg.display.flip()
|
||
|
|
||
|
# load the sound effects
|
||
|
boom_sound = load_sound("boom.wav")
|
||
|
shoot_sound = load_sound("car_door.wav")
|
||
|
if pg.mixer:
|
||
|
music = os.path.join(main_dir, "data", "house_lo.wav")
|
||
|
pg.mixer.music.load(music)
|
||
|
pg.mixer.music.play(-1)
|
||
|
|
||
|
# Initialize Game Groups
|
||
|
aliens = pg.sprite.Group()
|
||
|
shots = pg.sprite.Group()
|
||
|
bombs = pg.sprite.Group()
|
||
|
all = pg.sprite.RenderUpdates()
|
||
|
lastalien = pg.sprite.GroupSingle()
|
||
|
|
||
|
# Create Some Starting Values
|
||
|
alienreload = ALIEN_RELOAD
|
||
|
clock = pg.time.Clock()
|
||
|
|
||
|
# initialize our starting sprites
|
||
|
global SCORE
|
||
|
player = Player(all)
|
||
|
Alien(
|
||
|
aliens, all, lastalien
|
||
|
) # note, this 'lives' because it goes into a sprite group
|
||
|
if pg.font:
|
||
|
all.add(Score(all))
|
||
|
|
||
|
# Run our main loop whilst the player is alive.
|
||
|
while player.alive():
|
||
|
# get input
|
||
|
for event in pg.event.get():
|
||
|
if event.type == pg.QUIT:
|
||
|
return
|
||
|
if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
|
||
|
return
|
||
|
if event.type == pg.KEYDOWN:
|
||
|
if event.key == pg.K_f:
|
||
|
if not fullscreen:
|
||
|
print("Changing to FULLSCREEN")
|
||
|
screen_backup = screen.copy()
|
||
|
screen = pg.display.set_mode(
|
||
|
SCREENRECT.size, winstyle | pg.FULLSCREEN, bestdepth
|
||
|
)
|
||
|
screen.blit(screen_backup, (0, 0))
|
||
|
else:
|
||
|
print("Changing to windowed mode")
|
||
|
screen_backup = screen.copy()
|
||
|
screen = pg.display.set_mode(
|
||
|
SCREENRECT.size, winstyle, bestdepth
|
||
|
)
|
||
|
screen.blit(screen_backup, (0, 0))
|
||
|
pg.display.flip()
|
||
|
fullscreen = not fullscreen
|
||
|
|
||
|
keystate = pg.key.get_pressed()
|
||
|
|
||
|
# clear/erase the last drawn sprites
|
||
|
all.clear(screen, background)
|
||
|
|
||
|
# update all the sprites
|
||
|
all.update()
|
||
|
|
||
|
# handle player input
|
||
|
direction = keystate[pg.K_RIGHT] - keystate[pg.K_LEFT]
|
||
|
player.move(direction)
|
||
|
firing = keystate[pg.K_SPACE]
|
||
|
if not player.reloading and firing and len(shots) < MAX_SHOTS:
|
||
|
Shot(player.gunpos(), shots, all)
|
||
|
if pg.mixer and shoot_sound is not None:
|
||
|
shoot_sound.play()
|
||
|
player.reloading = firing
|
||
|
|
||
|
# Create new alien
|
||
|
if alienreload:
|
||
|
alienreload = alienreload - 1
|
||
|
elif not int(random.random() * ALIEN_ODDS):
|
||
|
Alien(aliens, all, lastalien)
|
||
|
alienreload = ALIEN_RELOAD
|
||
|
|
||
|
# Drop bombs
|
||
|
if lastalien and not int(random.random() * BOMB_ODDS):
|
||
|
Bomb(lastalien.sprite, all, bombs, all)
|
||
|
|
||
|
# Detect collisions between aliens and players.
|
||
|
for alien in pg.sprite.spritecollide(player, aliens, 1):
|
||
|
if pg.mixer and boom_sound is not None:
|
||
|
boom_sound.play()
|
||
|
Explosion(alien, all)
|
||
|
Explosion(player, all)
|
||
|
SCORE = SCORE + 1
|
||
|
player.kill()
|
||
|
|
||
|
# See if shots hit the aliens.
|
||
|
for alien in pg.sprite.groupcollide(aliens, shots, 1, 1).keys():
|
||
|
if pg.mixer and boom_sound is not None:
|
||
|
boom_sound.play()
|
||
|
Explosion(alien, all)
|
||
|
SCORE = SCORE + 1
|
||
|
|
||
|
# See if alien bombs hit the player.
|
||
|
for bomb in pg.sprite.spritecollide(player, bombs, 1):
|
||
|
if pg.mixer and boom_sound is not None:
|
||
|
boom_sound.play()
|
||
|
Explosion(player, all)
|
||
|
Explosion(bomb, all)
|
||
|
player.kill()
|
||
|
|
||
|
# draw the scene
|
||
|
dirty = all.draw(screen)
|
||
|
pg.display.update(dirty)
|
||
|
|
||
|
# cap the framerate at 40fps. Also called 40HZ or 40 times per second.
|
||
|
clock.tick(40)
|
||
|
|
||
|
if pg.mixer:
|
||
|
pg.mixer.music.fadeout(1000)
|
||
|
pg.time.wait(1000)
|
||
|
|
||
|
|
||
|
# call the "main" function if running this script
|
||
|
if __name__ == "__main__":
|
||
|
main()
|
||
|
pg.quit()
|