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.
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
beep.wav (Size: 4.65 KB / Downloads: 930)
Now we are going to add sound effects to the players lasers. Anytime the user fires a laser, it plays this sound effect.
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
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.
Go to part 8
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
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
Recommended Tutorials: