Python Forum
[PyGame] Flair and Organizing (part 7)
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Flair and Organizing (part 7)
#1
Back to part 6
https://python-forum.io/Thread-PyGame-Enemy-AI-part-6


This is going to be about the bells and whistles around the actual game. In our tutorial series, we have the basics of a bare bones shooter game. The player can shoot and kill enemies, and the enemies can shoot the player. It is very bland. But we can add some flair to it even though there is not much to the game play. Things like sound effects, score counter, and unlimited enemies can really add pizzazz to such a simple game. Additional animations and/or music can make it even better.

Organizing code

Now we are going to do some code shifting. This is just reorganizing our code. Our main game loop is starting to get cluttered, and we need to clean that out. There are a lot of enemy global functions on handling the numerous enemies. So we are going to make a class to control all the enemies. This will organize all this into this new class, and at the same time remove it from our main game loop.

I was going to wait to organize this later in a structure tutorial. But sometimes you just have to do it on the fly to keep your sanity. Also if we were going to make more types of enemies or different features such as personalities, etc. we would want the enemy controller class to handle such things and keep it organized. The longer you wait to organize code, the larger you code gets and the more likely it is you never do it.

import pygame as pg
import math
import random
    
pg.init()
  
class Enemy:
    total = 3
    def __init__(self, images, screen_rect, start_pos):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(start_pos[0], start_pos[1])
        )
        self.distance_above_player = 100 
        self.speed = 2
        self.bullet_color = (255,0,0)
        self.is_hit = False
        self.range_to_fire = False
        self.timer = 0.0
        self.bullets = [ ]
        self.dead = False
          
    def pos_towards_player(self, player_rect):
        c = math.sqrt((player_rect.x - self.rect.x) ** 2 + (player_rect.y - self.distance_above_player  - self.rect.y) ** 2)
        try:
            x = (player_rect.x - self.rect.x) / c
            y = ((player_rect.y - self.distance_above_player)  - self.rect.y) / c
        except ZeroDivisionError: 
            return False
        return (x,y)
          
    def update(self, dt, player):
        new_pos = self.pos_towards_player(player.rect)
        if new_pos: #if not ZeroDivisonError
            self.rect.x, self.rect.y = (self.rect.x + new_pos[0] * self.speed, self.rect.y + new_pos[1] * self.speed)
          
        self.check_attack_ability(player)
        if self.range_to_fire:  
            if pg.time.get_ticks() - self.timer > 1500.0:
                self.timer = pg.time.get_ticks()
                self.bullets.append(Laser(self.rect.center, self.bullet_color))
                  
        self.update_bullets(player)
                  
    def draw(self, surf):
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.rect)
        surf.blit(self.image, self.rect)
          
    def check_attack_ability(self, player):
        #if player is lower than enemy
        if player.rect.y >= self.rect.y: 
            try:
                offset_x =  self.rect.x - player.rect.x
                offset_y =  self.rect.y - player.rect.y
                d = int(math.degrees(math.atan(offset_x / offset_y)))
            except ZeroDivisionError: #player is above enemy
                return
            #if player is within 15 degrees lower of enemy
            if math.fabs(d) <= 15: 
                self.range_to_fire = True
            else:
                self.range_to_fire = False
                  
    def update_bullets(self, player):
        if self.bullets:
            for obj in self.bullets[:]:
                obj.update('down')
                #check collision
                if obj.rect.colliderect(player.rect):
                    offset_x =  obj.rect.x - player.rect.x 
                    offset_y =  obj.rect.y - player.rect.y
                    if player.mask.overlap(obj.mask, (offset_x, offset_y)):
                        player.take_damage(1)
                        self.bullets.remove(obj)
          
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert_alpha()
        #self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
    
    def update(self,direction='up'):
        if direction == 'down':
            self.rect.y += self.speed
        else:
            self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
    
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.dy = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False
        self.damage = 10
        
    def take_damage(self, value):
        self.damage -= value
    
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False
    
    def update(self, keys, dt, enemies):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        if keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        if keys[pg.K_UP]:
            self.rect.y -= self.dy * dt
        if keys[pg.K_DOWN]:
            self.rect.y += self.dy * dt
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
            
        self.check_laser_collision(enemies)
        
    def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        e.dead = True
                        self.lasers.remove(laser)
                        break #otherwise would create a ValueError: list.remove(x): x not in list
            
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
    
class EnemyController:
    def __init__(self):
        self.enemies = [ ]
        self.enemy_image = self.enemy_image_load()
        self.max_enemies = 3
        for i in range(self.max_enemies):
            self.enemies.append(self.randomized_enemy())
        
    def enemy_image_load(self):
        image = pg.image.load('enemy.png').convert()
        image.set_colorkey((255,0,255))
        transformed_image = pg.transform.rotate(image, 180)
        orig_image = pg.transform.scale(transformed_image, (40,80))
        mask = pg.mask.from_surface(orig_image)
        return (orig_image, mask)
            
    def randomized_enemy(self):
        y = random.randint(-500, -100) #above screen
        x = random.randint(0, screen_rect.width)
        return Enemy(self.enemy_image, screen_rect, (x,y))
        
    def update(self, dt, player):
        for e in self.enemies[:]:
            e.update(dt, player)
            if e.dead:
                self.enemies.remove(e)
                self.enemies.append(self.randomized_enemy())
            e.draw(screen)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
enemy_control = EnemyController()
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time, enemy_control.enemies)
    enemy_control.update(delta_time, player)
    player.draw(screen)
    pg.display.update()
Now we could add more types of enemies with different personalities, images, movement patterns, etc. with ease if we wanted to. Our main game loop is minimized and ready for more things to be implemented into our game.

In a future tutorial we will further organize our code into modules. But i will leave that for last only because its easier to do a tutorial in one file than multiple files. Ideally you want to split this single file up. Enemy things go into one, player another, a main control for the main game loop for the other, a root file to start the main game loop to allow users to easily located and run the file instead of looking through a list of files. But i will leave that discussion for later.

Sound effects

.wav   beep.wav (Size: 4.65 KB / Downloads: 785)
Now we are going to add sound effects to the players lasers. Anytime the user fires a laser, it plays this sound effect.

import pygame as pg
import math
import random
import os

pg.mixer.pre_init(44100, -16, 1, 512)
pg.init()
  
class Enemy:
    total = 3
    def __init__(self, images, screen_rect, start_pos):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(start_pos[0], start_pos[1])
        )
        self.distance_above_player = 100 
        self.speed = 2
        self.bullet_color = (255,0,0)
        self.is_hit = False
        self.range_to_fire = False
        self.timer = 0.0
        self.bullets = [ ]
        self.dead = False
          
    def pos_towards_player(self, player_rect):
        c = math.sqrt((player_rect.x - self.rect.x) ** 2 + (player_rect.y - self.distance_above_player  - self.rect.y) ** 2)
        try:
            x = (player_rect.x - self.rect.x) / c
            y = ((player_rect.y - self.distance_above_player)  - self.rect.y) / c
        except ZeroDivisionError: 
            return False
        return (x,y)
          
    def update(self, dt, player):
        new_pos = self.pos_towards_player(player.rect)
        if new_pos: #if not ZeroDivisonError
            self.rect.x, self.rect.y = (self.rect.x + new_pos[0] * self.speed, self.rect.y + new_pos[1] * self.speed)
          
        self.check_attack_ability(player)
        if self.range_to_fire:  
            if pg.time.get_ticks() - self.timer > 1500.0:
                self.timer = pg.time.get_ticks()
                self.bullets.append(Laser(self.rect.center, self.bullet_color))
                  
        self.update_bullets(player)
                  
    def draw(self, surf):
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.rect)
        surf.blit(self.image, self.rect)
          
    def check_attack_ability(self, player):
        #if player is lower than enemy
        if player.rect.y >= self.rect.y: 
            try:
                offset_x =  self.rect.x - player.rect.x
                offset_y =  self.rect.y - player.rect.y
                d = int(math.degrees(math.atan(offset_x / offset_y)))
            except ZeroDivisionError: #player is above enemy
                return
            #if player is within 15 degrees lower of enemy
            if math.fabs(d) <= 15: 
                self.range_to_fire = True
            else:
                self.range_to_fire = False
                  
    def update_bullets(self, player):
        if self.bullets:
            for obj in self.bullets[:]:
                obj.update('down')
                #check collision
                if obj.rect.colliderect(player.rect):
                    offset_x =  obj.rect.x - player.rect.x 
                    offset_y =  obj.rect.y - player.rect.y
                    if player.mask.overlap(obj.mask, (offset_x, offset_y)):
                        player.take_damage(1)
                        self.bullets.remove(obj)
          
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert_alpha()
        #self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
    
    def update(self,direction='up'):
        if direction == 'down':
            self.rect.y += self.speed
        else:
            self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
    
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.dy = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False
        self.damage = 10
        
    def take_damage(self, value):
        self.damage -= value
    
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False
                    TOOLS.laser.play()
    
    def update(self, keys, dt, enemies):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        if keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        if keys[pg.K_UP]:
            self.rect.y -= self.dy * dt
        if keys[pg.K_DOWN]:
            self.rect.y += self.dy * dt
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
            
        self.check_laser_collision(enemies)
        
    def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        e.dead = True
                        self.lasers.remove(laser)
                        break #otherwise would create a ValueError: list.remove(x): x not in list
            
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
    
class EnemyController:
    def __init__(self):
        self.enemies = [ ]
        self.enemy_image = self.enemy_image_load()
        self.max_enemies = 3
        for i in range(self.max_enemies):
            self.enemies.append(self.randomized_enemy())
        
    def enemy_image_load(self):
        image = pg.image.load('enemy.png').convert()
        image.set_colorkey((255,0,255))
        transformed_image = pg.transform.rotate(image, 180)
        orig_image = pg.transform.scale(transformed_image, (40,80))
        mask = pg.mask.from_surface(orig_image)
        return (orig_image, mask)
            
    def randomized_enemy(self):
        y = random.randint(-500, -100) #above screen
        x = random.randint(0, screen_rect.width)
        return Enemy(self.enemy_image, screen_rect, (x,y))
        
    def update(self, dt, player):
        for e in self.enemies[:]:
            e.update(dt, player)
            if e.dead:
                self.enemies.remove(e)
                self.enemies.append(self.randomized_enemy())
            e.draw(screen)
            
class Tools:
    def __init__(self):
        self.sound_init()
        
    def sound_init(self):
        directory_of_sounds = '.' #current directory == '.'
        self.laser = self.load_sound_file('beep.wav', directory_of_sounds)
        self.laser.set_volume(.1)
        
    def load_sound_file(self, filename, directory):
        fullname = os.path.join(directory, filename)
        return pg.mixer.Sound(fullname)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
TOOLS = Tools()
player = Player(screen_rect)
enemy_control = EnemyController()
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time, enemy_control.enemies)
    enemy_control.update(delta_time, player)
    player.draw(screen)
    pg.display.update()
Here we create yet another class called Tools. It loads our sound file, sets the volume, and uses it make a pygame Sound object. This object we can just call object.play() when we want it played. In our case we want it played when the player shoots. So in our get_event method of the player we play the sound when the laser object gets added since the laser starts moving right away once its in that list.

    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False
                    TOOLS.laser.play()
An often problem is a delay on the sound. This can often be remedied by changing the pre_init values before pygame.init()
pg.mixer.pre_init(44100, -16, 1, 512)
pg.init()
This fixes our sound to be played as soon as we hit the spacebar instead of giving a delay.

Displaying Score text on screen
We are adding the boilerplate code regarding creating font creation to the Tools class. We are also using SysFont instead of loading a font file to make it simpler. You should always use a font file in real life. There are some issues when using SysFont and building an .exe for example. In Enemy.update we actually add the score that enemy gives to the player. We created a Player.score and a method to add to it that also updates the text on the screen

    def add_score(self, amt):
        self.score += amt
        TOOLS.update_text(self.score)
instead of running this in the main game loop, we can just change the text when the score actually changes. No need to create a possible bottleneck.

import pygame as pg
import math
import random
import os

pg.mixer.pre_init(44100, -16, 1, 512)
pg.init()
  
class Enemy:
    total = 3
    def __init__(self, images, screen_rect, start_pos):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(start_pos[0], start_pos[1])
        )
        self.distance_above_player = 100 
        self.speed = 2
        self.bullet_color = (255,0,0)
        self.is_hit = False
        self.range_to_fire = False
        self.timer = 0.0
        self.bullets = [ ]
        self.dead = False
          
    def pos_towards_player(self, player_rect):
        c = math.sqrt((player_rect.x - self.rect.x) ** 2 + (player_rect.y - self.distance_above_player  - self.rect.y) ** 2)
        try:
            x = (player_rect.x - self.rect.x) / c
            y = ((player_rect.y - self.distance_above_player)  - self.rect.y) / c
        except ZeroDivisionError: 
            return False
        return (x,y)
          
    def update(self, dt, player):
        new_pos = self.pos_towards_player(player.rect)
        if new_pos: #if not ZeroDivisonError
            self.rect.x, self.rect.y = (self.rect.x + new_pos[0] * self.speed, self.rect.y + new_pos[1] * self.speed)
          
        self.check_attack_ability(player)
        if self.range_to_fire:  
            if pg.time.get_ticks() - self.timer > 1500.0:
                self.timer = pg.time.get_ticks()
                self.bullets.append(Laser(self.rect.center, self.bullet_color))
                  
        self.update_bullets(player)
                  
    def draw(self, surf):
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.rect)
        surf.blit(self.image, self.rect)
          
    def check_attack_ability(self, player):
        #if player is lower than enemy
        if player.rect.y >= self.rect.y: 
            try:
                offset_x =  self.rect.x - player.rect.x
                offset_y =  self.rect.y - player.rect.y
                d = int(math.degrees(math.atan(offset_x / offset_y)))
            except ZeroDivisionError: #player is above enemy
                return
            #if player is within 15 degrees lower of enemy
            if math.fabs(d) <= 15: 
                self.range_to_fire = True
            else:
                self.range_to_fire = False
                  
    def update_bullets(self, player):
        if self.bullets:
            for obj in self.bullets[:]:
                obj.update('down')
                #check collision
                if obj.rect.colliderect(player.rect):
                    offset_x =  obj.rect.x - player.rect.x 
                    offset_y =  obj.rect.y - player.rect.y
                    if player.mask.overlap(obj.mask, (offset_x, offset_y)):
                        player.take_damage(1)
                        self.bullets.remove(obj)
          
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert_alpha()
        #self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
    
    def update(self,direction='up'):
        if direction == 'down':
            self.rect.y += self.speed
        else:
            self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
    
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.dy = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False
        self.damage = 10
        self.score = 0
        
    def take_damage(self, value):
        self.damage -= value
    
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False
                    TOOLS.laser.play()
    
    def update(self, keys, dt, enemies):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        if keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        if keys[pg.K_UP]:
            self.rect.y -= self.dy * dt
        if keys[pg.K_DOWN]:
            self.rect.y += self.dy * dt
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
            
        self.check_laser_collision(enemies)
        
    def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        e.dead = True
                        self.lasers.remove(laser)
                        break #otherwise would create a ValueError: list.remove(x): x not in list
            
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
        
    def add_score(self, amt):
        self.score += amt
        TOOLS.update_text(self.score)
    
class EnemyController:
    def __init__(self):
        self.enemies = [ ]
        self.enemy_image = self.enemy_image_load()
        self.max_enemies = 3
        for i in range(self.max_enemies):
            self.enemies.append(self.randomized_enemy())
        
    def enemy_image_load(self):
        image = pg.image.load('enemy.png').convert()
        image.set_colorkey((255,0,255))
        transformed_image = pg.transform.rotate(image, 180)
        orig_image = pg.transform.scale(transformed_image, (40,80))
        mask = pg.mask.from_surface(orig_image)
        return (orig_image, mask)
            
    def randomized_enemy(self):
        y = random.randint(-500, -100) #above screen
        x = random.randint(0, screen_rect.width)
        return Enemy(self.enemy_image, screen_rect, (x,y))
        
    def update(self, dt, player):
        for e in self.enemies[:]:
            e.update(dt, player)
            if e.dead:
                self.enemies.remove(e)
                player.add_score(25)
                self.enemies.append(self.randomized_enemy())
            e.draw(screen)
            
class Tools:
    def __init__(self, screen):
        self.screen = screen
        self.sound_init()
        self.font_init(screen.get_rect())
        
    def font_init(self, screen_rect):
        self.text_size = 15
        self.text_color = (255,255,255)
        self.topleft_pos = (10,10)
        self.font = pg.font.SysFont('Arial', self.text_size)
        self.update_text()
        
    def update_text(self, score=0):
        self.text, self.text_rect = self.make_text('Score: {}'.format(score))
        
    def make_text(self,message):
        text = self.font.render(message,True,self.text_color)
        rect = text.get_rect(topleft=self.topleft_pos)
        return text,rect
        
    def draw(self):
        self.screen.blit(self.text, self.text_rect)
        
    def sound_init(self):
        directory_of_sounds = '.' #current directory == '.'
        self.laser = self.load_sound_file('beep.wav', directory_of_sounds)
        self.laser.set_volume(.1)
        
    def load_sound_file(self, filename, directory):
        fullname = os.path.join(directory, filename)
        return pg.mixer.Sound(fullname)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
TOOLS = Tools(screen)
player = Player(screen_rect)
enemy_control = EnemyController()
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time, enemy_control.enemies)
    enemy_control.update(delta_time, player)
    player.draw(screen)
    TOOLS.draw()
    pg.display.update()
This will create a label in the topleft corner of a score number adding 25 every time you shoot an enemy.

Player Death
Actually killing the player is easy. You can just pause the game (or in a more complex game go back to menu) when they take enough hits. In our case we just added a player.dead attribute and if his damage goes below 0 then he is dead. Before we added player.take_damage(1) in Enemy.update_bullets method. This before decreased a number but never did anything. Now when that number reaches 0, the game freezes. We can add some flair to this such as a damage counter and a game over text. The first is basically identical to the previous text we added. And we are going to reuse some of that.

import pygame as pg
import math
import random
import os

pg.mixer.pre_init(44100, -16, 1, 512)
pg.init()
  
class Enemy:
    total = 3
    def __init__(self, images, screen_rect, start_pos):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(start_pos[0], start_pos[1])
        )
        self.distance_above_player = 100 
        self.speed = 2
        self.bullet_color = (255,0,0)
        self.is_hit = False
        self.range_to_fire = False
        self.timer = 0.0
        self.bullets = [ ]
        self.dead = False
          
    def pos_towards_player(self, player_rect):
        c = math.sqrt((player_rect.x - self.rect.x) ** 2 + (player_rect.y - self.distance_above_player  - self.rect.y) ** 2)
        try:
            x = (player_rect.x - self.rect.x) / c
            y = ((player_rect.y - self.distance_above_player)  - self.rect.y) / c
        except ZeroDivisionError: 
            return False
        return (x,y)
          
    def update(self, dt, player):
        new_pos = self.pos_towards_player(player.rect)
        if new_pos: #if not ZeroDivisonError
            self.rect.x, self.rect.y = (self.rect.x + new_pos[0] * self.speed, self.rect.y + new_pos[1] * self.speed)
          
        self.check_attack_ability(player)
        if self.range_to_fire:  
            if pg.time.get_ticks() - self.timer > 1500.0:
                self.timer = pg.time.get_ticks()
                self.bullets.append(Laser(self.rect.center, self.bullet_color))
                  
        self.update_bullets(player)
                  
    def draw(self, surf):
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.rect)
        surf.blit(self.image, self.rect)
          
    def check_attack_ability(self, player):
        #if player is lower than enemy
        if player.rect.y >= self.rect.y: 
            try:
                offset_x =  self.rect.x - player.rect.x
                offset_y =  self.rect.y - player.rect.y
                d = int(math.degrees(math.atan(offset_x / offset_y)))
            except ZeroDivisionError: #player is above enemy
                return
            #if player is within 15 degrees lower of enemy
            if math.fabs(d) <= 15: 
                self.range_to_fire = True
            else:
                self.range_to_fire = False
                  
    def update_bullets(self, player):
        if self.bullets:
            for obj in self.bullets[:]:
                obj.update('down')
                #check collision
                if obj.rect.colliderect(player.rect):
                    offset_x =  obj.rect.x - player.rect.x 
                    offset_y =  obj.rect.y - player.rect.y
                    if player.mask.overlap(obj.mask, (offset_x, offset_y)):
                        player.take_damage(1)
                        self.bullets.remove(obj)
          
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert_alpha()
        #self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
    
    def update(self,direction='up'):
        if direction == 'down':
            self.rect.y += self.speed
        else:
            self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
    
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.mask = pg.mask.from_surface(self.image)
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.dy = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False
        self.damage = 10
        self.score = 0
        self.dead = False
        
    def take_damage(self, value):
        self.damage -= value
        TOOLS.update_damage(self.damage)
    
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False
                    TOOLS.laser.play()
    
    def update(self, keys, dt, enemies):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        if keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        if keys[pg.K_UP]:
            self.rect.y -= self.dy * dt
        if keys[pg.K_DOWN]:
            self.rect.y += self.dy * dt
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
            
        self.check_laser_collision(enemies)
        
        if self.damage <= 0:
            self.damage = 0
            self.dead = True
            TOOLS.game_over_sound.play()
        
    def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        e.dead = True
                        self.lasers.remove(laser)
                        break #otherwise would create a ValueError: list.remove(x): x not in list
            
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
        
    def add_score(self, amt):
        self.score += amt
        TOOLS.update_text(self.score)
    
class EnemyController:
    def __init__(self):
        self.enemies = [ ]
        self.enemy_image = self.enemy_image_load()
        self.max_enemies = 3
        for i in range(self.max_enemies):
            self.enemies.append(self.randomized_enemy())
        
    def enemy_image_load(self):
        image = pg.image.load('enemy.png').convert()
        image.set_colorkey((255,0,255))
        transformed_image = pg.transform.rotate(image, 180)
        orig_image = pg.transform.scale(transformed_image, (40,80))
        mask = pg.mask.from_surface(orig_image)
        return (orig_image, mask)
            
    def randomized_enemy(self):
        y = random.randint(-500, -100) #above screen
        x = random.randint(0, screen_rect.width)
        return Enemy(self.enemy_image, screen_rect, (x,y))
        
    def update(self, dt, player):
        for e in self.enemies[:]:
            e.update(dt, player)
            if e.dead:
                self.enemies.remove(e)
                player.add_score(25)
                self.enemies.append(self.randomized_enemy())
            e.draw(screen)
            
class Tools:
    def __init__(self, screen):
        self.screen = screen
        self.screen_rect = screen.get_rect()
        self.sound_init()
        self.font_init(self.screen_rect)
        
    def font_init(self, screen_rect):
        self.text_size = 15
        self.text_color = (255,255,255)
        self.score_pos = (10,10)
        self.damage_pos = (10,30)
        self.font = pg.font.SysFont('Arial', self.text_size)
        self.update_text()
        self.update_damage()
        self.game_over()
        
    def update_text(self, score=0):
        self.text, self.text_rect = self.make_text('Score: {}'.format(score), self.score_pos)
        
    def update_damage(self, damage=None):
        self.damage, self.damage_rect = self.make_text('Damage: {}'.format(damage), self.damage_pos)
        
    def make_text(self,message, pos):
        text = self.font.render(message,True,self.text_color)
        rect = text.get_rect(topleft=pos)
        return text, rect
        
    def game_over(self):
        game_over_font = pg.font.SysFont('Arial', 45)
        self.game_over_text = game_over_font.render('Game Over',True,(255,0,0))
        self.game_over_rect = self.game_over_text.get_rect(center=self.screen_rect.center)
        
    def draw(self):
        self.screen.blit(self.text, self.text_rect)
        self.screen.blit(self.damage, self.damage_rect)
        
    def sound_init(self):
        directory_of_sounds = '.' #current directory == '.'
        self.laser = self.load_sound_file('beep.wav', directory_of_sounds)
        self.laser.set_volume(.1)
        self.game_over_sound = self.load_sound_file('game_over.wav', directory_of_sounds)
        self.game_over_sound.set_volume(.2)
        
    def load_sound_file(self, filename, directory):
        fullname = os.path.join(directory, filename)
        return pg.mixer.Sound(fullname)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
TOOLS = Tools(screen)
player = Player(screen_rect)
enemy_control = EnemyController()
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    if not player.dead:
        player.update(keys, delta_time, enemy_control.enemies)
        enemy_control.update(delta_time, player)
    else:
        screen.blit(TOOLS.game_over_text, TOOLS.game_over_rect)
    player.draw(screen)
    TOOLS.draw()
    pg.display.update()
At this point our code is getting so long that it is becoming hard to code. We need to break this up into individual files before we can add any more.

Go to part 8

Attached Files

.wav   explosion_with_debris.wav (Size: 270.84 KB / Downloads: 760)
.wav   game_over.wav (Size: 116.58 KB / Downloads: 770)
Recommended Tutorials:
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] Structure and Organizing (part 8) metulburr 0 6,870 Nov-29-2017, 10:18 PM
Last Post: metulburr

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020