Python Forum

Full Version: Enemy AI and collision (part 6)
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Back to part 5
https://python-forum.io/Thread-PyGame-Ba...ion-part-5

We are moving back to our spaceship game now.

This is where our code is going to get much longer. Up to now we have been able to keep it under 100 lines of code. The enemy class is going to make our game more complicated. By this point you should be considering moving Player class to a module name player.py. Maybe Laset class to a module as well depending. And then creating a new module for enemy class. To have everything all in one file makes it hard to find code, especially when they do not related to each other. However in this tutorial I will not do this as the cutoff point is directly after the enemy.

Best way to start this is by adding an enemy image in a stationary location, and add things one piece at a time.

[attachment=283]
If we run the following there are some obvious issues such as the image needs to be scaled down a lot. But we are going to leave that for a moment to discuss other issues.
import pygame as pg
  
pg.init()

class Enemy:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('enemy.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = -100
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        
    def update(self, dt):
        pass
        
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
        
        
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
  
    def update(self):
        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.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
  
    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):
        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
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
  
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
enemy = Enemy(screen_rect)
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.update(delta_time)
    player.draw(screen)
    enemy.draw(screen)
    pg.display.update()
This code is added the up and down movements of the Player class. This is the only time you should have a list of if conditions with no elif as in Player.update. We also added the basic enemy class and the code required to display it on screen.

Creating bottlenecks by improperly handling images
We have loaded the image inside the Enemy class just like we did in player class. However this is not the best approach. There are going to be tens to hundreds of enemies on the screen at any one time. Each time you create an enemy object, it loads the same image for each one. There is no reason to waste resources loading the same image over and over again. This could be a bottleneck in the future. The player doesnt really matter as you are only loading the image one (assuming there is one player). But you could remove that as well to make it more uniformed.

ENEMY_IMAGE = pg.image.load('enemy.png').convert()

class Enemy:
    def __init__(self, image, screen_rect):
        self.screen_rect = screen_rect
        self.image = image
...
enemy = Enemy(ENEMY_IMAGE, screen_rect)
This is at least what you want to do. If your doing a lot of scaling, rotating, or other image modifications, it would be a good idea to do that outside of the dunder init method as well and pass the final result to it instead to avoid any more bottlenecks.

def enemy_image_load():
    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))
    return orig_image
    
...
ENEMY_IMAGE = enemy_image_load()
enemy = Enemy(ENEMY_IMAGE, screen_rect)
Now the enemy_image_load function runs only once. And you can create as many Enemy objects without having to worry about creating a bottleneck.

Enemy moves towards player
The enemy needs to move towards the player to feel like AI. You can add a bunch of different clauses here and exceptions, etc. But we are just simply going to move the enemy above the player so they can shoot at the player. Above the player is going to be their ideal location. In our case 100 pixels above the player.

import pygame as pg
import math
  
pg.init()

def enemy_image_load():
    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))
    return orig_image

class Enemy:
    def __init__(self, image, screen_rect):
        self.screen_rect = screen_rect
        self.image = image
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.distance_above_player = 100 
        self.speed = 2
        
    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):
        #move enemy towards 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)
        
    def draw(self, surf):
        surf.blit(self.image, self.rect)
        
        
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
  
    def update(self):
        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.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
  
    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):
        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
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
  
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemy = Enemy(ENEMY_IMAGE, screen_rect)
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.update(delta_time, player)
    player.draw(screen)
    enemy.draw(screen)
    pg.display.update()
As you can see the enemy follows the player to get above them where ever they go. We need to reduce the distance between the enemy and player by changing the enemy's position. The method pos_towards_player gets the next position towards the player from the position of where that enemy is currently located at.
Quote:Create a vector in the direction that you want the enemy to move. That's easy:

dir.x = player.x - enemy.x;
dir.y = player.y - enemy.y;
Now normalize this vector. That means divide the terms by the magnitude (the hypotenuse) of the vector.

hyp = sqrt(dir.x*dir.x + dir.y*dir.y);
dir.x /= hyp;
dir.y /= hyp;
Now you just need to add that vector to the enemy's position, multiplied by the speed you want the enemy to move:

enemy.x += dir.x*speed;
enemy.y += dir.y*speed;
Here's how it works - if you add that initial vector to the enemy's position it will instantly be transported to the player. You obviously want to enemy to move at a slower speed. When you normalize the vector, you make it's magnitude (essentially the hypotenuse of the triangle it forms) equal to 1. So now, adding the direction vector moves the enemy by one unit. Multiply that 1 unit by the enemy's speed, and now it's moving at the correct speed.

Enemy attack
The following code shoots a laser if the player is 15 degrees below him.
import pygame as pg
import math
  
pg.init()

def enemy_image_load():
    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))
    return orig_image

class Enemy:
    def __init__(self, image, screen_rect):
        self.screen_rect = screen_rect
        self.image = image
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        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 = [ ]
        
    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):
        surf.blit(self.image, self.rect)
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.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')
        
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        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.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
  
    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):
        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
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
  
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemy = Enemy(ENEMY_IMAGE, screen_rect)
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.update(delta_time, player)
    player.draw(screen)
    enemy.draw(screen)
    pg.display.update()
The enemy check_attack_abilty method sees if the player is below him, and within 15 degrees of him. As there is no point in shooting at the player if he is on the other side of the screen. If it checks out a Laser object gets added to the the bullet list. The update_bullets method just runs the update method for the laser class. The laser class also as a condition added to make the laser go up if its the player, and down if its the enemy.

The obvious thing now that is missing is the collision detection between the ships and the lasers.

Mask collision
The next thing is adding mask collision. The best way to understand the difference between rect and masks collisions is masks are pixel perfect, where rects are the image as a whole. Now we could only add rect collisions but the lasers would hit the edge of the image. Which in our case the image and what is showing is two different things. In the image in the beginning of this tutorial shows the enemy ship with a pink background. If a laser is detecting rect collision anytime it hits that image (including the pink part), it will consider it hit the ship. Whereas a mask is the ship itself, or what is actually shown on screen.

Detecting mask collision is demanding. You should always first check rect collision, then mask collision. The reason why is there is no point is wasting resource searching pixel by pixel, if the two object you are checking are on opposite sides of the screen. Its another bottleneck that could appear. That way if they are too far apart, the rect collision will detect it and pass by the mask check (ie, saving resources).

You can play the differences between rect collision and mask collision here. Download the repo and run fall_rect.py and then run fall_mask.py. You will see how masks are better in comparison to detail.


So here we updated all classes here to have a mask from the image. We are just doing rect collision to show a point at first. The below code adds content into Enemy.update_bullet method to handle bullet collision with the player. Here we put the meat of the code into Enemy, but you could of put it into Laser class, or even Player class. It effects all so its a choice as to which one you want to put it into.

import pygame as pg
import math
    
pg.init()
  
def enemy_image_load():
    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)
  
class Enemy:
    def __init__(self, images, screen_rect):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        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 = [ ]
          
    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):
                    player.take_damage(1)
                    print(player.damage)
                    self.bullets.remove(obj)
                    break
          
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        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.mask = pg.mask.from_surface(self.image)
        self.image.set_colorkey((255,0,255))
        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):
        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
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
    
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemy = Enemy(ENEMY_IMAGE, screen_rect)
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.update(delta_time, player)
    player.draw(screen)
    enemy.draw(screen)
    pg.display.update()
You will see that the laser hits the image, but not the ship and it gets removed from play. Note that when you remove an object from a list you are iterating you must use a copy of that list via the [:]in the line for obj in self.bullets[:]:

The following shows masks being used in addition to the initial rect collision. First the rect check to make sure it is worth checking for masks, then the actual mask check. The meat of it is in Enemy.update_bullets()

    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)
It gets the offset of the two objects and checks if they are overlapping, otherwise returns None. If they collide, the player takes damage, and the object that collided gets removed.
full code:
import pygame as pg
import math
    
pg.init()
  
def enemy_image_load():
    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)
  
class Enemy:
    def __init__(self, images, screen_rect):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        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 = [ ]
          
    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):
        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
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True
    
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemy = Enemy(ENEMY_IMAGE, screen_rect)
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.update(delta_time, player)
    player.draw(screen)
    enemy.draw(screen)
    pg.display.update()
NOTE: the mask from surface must be after setting colorkey, and the laser is set to .convert_alpha since its a surface that has no alpha channel.

Remove enemy if they are hit

To make this even playable we have to finish it. What happens to the enemy if they are hit by players attack. Lets remove that enemy from game. We could make a fancy explosion, but we are just going to remove them to make it bare bones. We are going to do a similar check but in reverse...check if our laser hits the enemy. However remove the enemy if the laser hits them.

import pygame as pg
import math
    
pg.init()
  
def enemy_image_load():
    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)
  
class Enemy:
    def __init__(self, images, screen_rect):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        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)
            
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemies = []
enemies.append(Enemy(ENEMY_IMAGE, screen_rect))
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, enemies)
    for e in enemies[:]:
        e.update(delta_time, player)
        if e.dead:
            enemies.remove(e)
        e.draw(screen)
    player.draw(screen)
    pg.display.update()
We did some changes to our main game loop

ENEMY_IMAGE = enemy_image_load()
enemies = []
enemies.append(Enemy(ENEMY_IMAGE, screen_rect))
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, enemies)
    for e in enemies[:]:
        e.update(delta_time, player)
        if e.dead:
            enemies.remove(e)
        e.draw(screen)
    player.draw(screen)
    pg.display.update()
We are creating an enemy list, and removing that enemy from the list if they get hit. If they are removed from the list, they dont blit, or take action, they dont exist.

In player class we created a collision to rect and mask almost identical to previously.

Multiple Enemies

Now we are going to move the enemy outside of field of view and add randomly generator enemies off screen. Their starting position will indicate where they are coming from and how long it takes them to get on screen or to the player.

import pygame as pg
import math
import random
    
pg.init()
  
def enemy_image_load():
    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)
  
class Enemy:
    total = 10
    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)
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemies = []
for i in range(Enemy.total):
    y = random.randint(-500, -100) #above screen
    x = random.randint(0, screen_rect.width)
    enemies.append(Enemy(ENEMY_IMAGE, screen_rect, (x,y)))
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, enemies)
    for e in enemies[:]:
        e.update(delta_time, player)
        if e.dead:
            enemies.remove(e)
        e.draw(screen)
    player.draw(screen)
    pg.display.update()
Additional AI thoughts

Now you will see that when we only had one enemy the 100 pixels above the player was OK. But with so many enemies the movement pattern is predictable and eventually all the enemies end up overlapping each other. You could add things like some enemies are 100px above while others are at 300px. Randomize it. Give some enemies this movement pattern while giving other enemies other movement patterns. Or some enemies might be slower while others are faster. Make some enemies stupid, and add a timer on them that slows their logic down, while making others predict the players movements heading in that direction instead. Make some enemies dodge the player attacks.

Give the enemies personality. Assuming the enemy hit count was not one, if the enemy gets hit by the player, have them shoot 3 times at once or increase their speed temporarily. Or if one enemy gets hit by another have them change target to that enemy for awhile aiding the player. If the enemy hits you have them do a dance (in this case rotate 360). If they make mistakes, they feel more like humans and not a deck of cards. If the enemies do things based on individual personality, then they become unpredictable and exciting, etc.

Adding different weapon types could also add different AI techniques. For example a shotgun type spread shot, the player wouldnt need to follow the player around. There are a lot of things you can do at this point. This tutorial is going to stop here regarding AI. I urge you to read up on books/blogs etc. about making AI.

Unlimited Enemies

As we have it there are only 10 enemies. Once you kill them all, you cant do anything else. We are going to limit the number of enemies down to 3 and make them unlimited. This alone makes their movement pattern not so predictable. And it keeps the game moving.

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)
        
def enemy_image_load():
    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():
    y = random.randint(-500, -100) #above screen
    x = random.randint(0, screen_rect.width)
    return Enemy(ENEMY_IMAGE, screen_rect, (x,y))
    
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemies = []
for i in range(Enemy.total):
    enemies.append(randomized_enemy())
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, enemies)
    for e in enemies[:]:
        e.update(delta_time, player)
        if e.dead:
            enemies.remove(e)
            enemies.append(randomized_enemy())
        e.draw(screen)
    player.draw(screen)
    pg.display.update()
Now the enemies come unlimited and at a max of 3 enemies. However adding this code has cluttered our main game loop and added another global function pertaining to the enemy. We will reorganize the code on the next tutorial.


Part 7
https://python-forum.io/Thread-PyGame-Fl...ing-part-7