1

For a game I'm creating, I have enemy ships that shoot lasers targeted to the player sprite. Those enemies rotate in order to face the player and then fire a projectile that aim to collide with the player ship.

However, those lasers sprites spawn currently at a fixed location, and thus, can overlap with the enemy ship hitbox when the player moves around, as you can see in this gif.

How could I make the spawning location move around the edge of the enemy sprites, so that the lasers are created outside of the enemy sprite?

The revelant part is inside the Shoot method of the DroneArc class at line 146 for the spawn location, and the Laser class at line 44.

Here is the simplified code that can be copied if needed:

import pygame
import sys
import time
import math

pygame.init()

WIDTH = 750
HEIGHT = 750
SIZE = WIDTH, HEIGHT 

CLOCK = pygame.time.Clock()
screen = pygame.display.set_mode(SIZE)

FPS = 120

def in_game():
    run = True
    global player
    player = MainShip(310,600)
    global enemies
    enemies = [Drone_Arc(50,50,(50,50),0),Drone_Arc(50,650,(50,50),0)]

    while run:
        screen.fill(0)
        player.x, player.y = pygame.mouse.get_pos()
        player.draw()

        for enemy in enemies[:]:
            angle =90-math.degrees(math.atan2((player.y+player.get_height()/2 - (enemy.y+enemy.get_height()/2)),(player.x+player.get_width()/2- (enemy.x+enemy.get_width()/2))))
            enemy.rotate_ship(angle)
            enemy.draw()
            enemy.shoot(angle)
            enemy.move_lasers(player)

            if collide(enemy,player):
                    enemies.remove(enemy)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
    
        pygame.display.update()
        CLOCK.tick(FPS)

def collide(obj1,obj2):
    offset_x = obj2.x - obj1.x
    offset_y = obj2.y - obj1.y
    return obj1.mask.overlap(obj2.mask, ((int(offset_x)), ((int(offset_y)))))

class Laser(pygame.sprite.Sprite):
    def __init__(self, x, y, w, h,dx,dy):
        pygame.sprite.Sprite.__init__(self)
        self.original_image = pygame.Surface([w, h], pygame.SRCALPHA)
        self.color = (255,0,0)
        self.original_image.fill(self.color)
        self.image = self.original_image
        self.mask = pygame.mask.from_surface(self.image)
        self.dx = dx
        self.dy = dy
        self.x = x
        self.y = y
        self.rect = self.image.get_rect(center = (self.x, self.y)) 

    def draw(self, screen):
        screen.blit(self.image, self.rect)

    def rotate_laser(self, angle):
        self.image = pygame.transform.rotate(self.original_image, angle)
        self.rect = self.image.get_rect(center = (self.x, self.y)) 
        self.mask = pygame.mask.from_surface(self.image)

    def move(self, speed):
        self.x += self.dx*speed
        self.y += self.dy*speed
        self.rect = self.image.get_rect(center = (self.x, self.y))

    def collision(self, obj):
        return collide(self, obj)


class Ship(pygame.sprite.Sprite):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.ship_img = None
        self.laser_img = None
        self.lasers = []
        self.cooldown_counter = 0

    def cooldown(self):
        if self.cooldown_counter >= self.COOLDOWN:
            self.cooldown_counter = 0
        elif self.cooldown_counter > 0:
            self.cooldown_counter += 1

    def get_width(self):
        return self.image.get_width()

    def get_height(self):
        return self.image.get_height()

    def rotate_ship(self,angle):

        w, h       = self.original_image.get_size()
        box        = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]
        box_rotate = [p.rotate(angle) for p in box]
        min_box    = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])
        max_box    = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])

        pivot        = pygame.math.Vector2(w/2, -h/2)
        pivot_rotate = pivot.rotate(angle)
        pivot_move   = pivot_rotate - pivot

        origin = (self.x - w/2 + min_box[0] - pivot_move[0], self.y - h/2 - max_box[1] + pivot_move[1])
        self.image = pygame.transform.rotate(self.original_image, angle)
        self.mask = pygame.mask.from_surface(self.image)

class MainShip(Ship): 
    COOLDOWN = 15
    def __init__(self, x, y):
        super().__init__(x, y)
        self.original_image = pygame.Surface([36, 62], pygame.SRCALPHA)
        self.color = (255,255,255)
        self.original_image.fill(self.color)
        self.image = self.original_image
        self.mask = pygame.mask.from_surface(self.image)

    def draw(self):
        screen.blit(self.image,(self.x,self.y))

class Drone_Arc(Ship):
    COOLDOWN = 50
    def __init__(self, x,y,path,speed):
        super().__init__(x,y)
        self.original_image = pygame.Surface([36, 36], pygame.SRCALPHA)
        self.color = (255,255,255)
        self.original_image.fill(self.color)
        self.image = self.original_image
        self.mask = pygame.mask.from_surface(self.image)
        self.laser_speed = 3
        self.angle = 0

    def shoot(self, angle):
        if self.cooldown_counter == 0:
            radians = math.atan2((player.y+player.get_height()/2 - (self.y+self.get_height()/2)),(player.x+player.get_width()/2- (self.x+self.get_width()/2)))
            dx = math.cos(radians)
            dy = math.sin(radians)
            
            cx, cy =  self.x + self.get_height() / 2, self.y + self.get_width()/2
            lx, ly = cx + dx * (self.get_height()/2 + 11), cy + dy * (self.get_height()/2 + 11)
            
            print(dx,dy)
            laser_1 = Laser(lx, ly, 81, 22,dx, dy)
            laser_1.rotate_laser(angle)
            self.lasers.append(laser_1)
            self.cooldown_counter = 1

    def move_lasers(self,obj):
        self.cooldown()
        for laser in self.lasers:
            laser.move(self.laser_speed)
            if laser.collision(obj):
                self.lasers.remove(laser)
    
    def draw(self):
        screen.blit(self.image, (self.x, self.y))

        pygame.draw.circle(screen,(255,0,0),(player.x + player.get_width()/2,player.y + player.get_height()/2),3)
        pygame.draw.circle(screen,(255,0,0),(self.x+self.get_width()/2,self.y + self.get_height()/2),3)
        pygame.draw.circle(screen,(255,0,0),((((player.x + player.get_height()/2)+(self.x-self.get_width()/2))/2),(((player.y + player.get_height()/2)+(self.y+self.get_height()/2))/2)),3)

        for laser in self.lasers:
            laser.draw(screen)

in_game()
Maximus
  • 37
  • 7

1 Answers1

1

Read How do I rotate an image around its center using PyGame? and apply the suggestions to the Laser class. This greatly simplifies your code:

class Laser(pygame.sprite.Sprite):
    def __init__(self, x, y, w, h,dx,dy):
        pygame.sprite.Sprite.__init__(self)
        self.original_image = pygame.Surface([w, h], pygame.SRCALPHA)
        self.color = (255,0,0)
        self.original_image.fill(self.color)
        self.image = self.original_image
        self.mask = pygame.mask.from_surface(self.image)
        self.dx = dx
        self.dy = dy
        self.x = x
        self.y = y
        self.rect = self.image.get_rect(center = (self.x, self.y)) 

    def draw(self, screen):
        screen.blit(self.image, self.rect)

    def rotate_laser(self, angle):
        self.image = pygame.transform.rotate(self.original_image, angle)
        self.rect = self.image.get_rect(center = (self.x, self.y)) 
        self.mask = pygame.mask.from_surface(self.image)

    def move(self, speed):
        self.x += self.dx*speed
        self.y += self.dy*speed
        self.rect = self.image.get_rect(center = (self.x, self.y)) 

In the new code, the attributes x and y specify the center point of the laser. When the laser shoots, all you have to do is calculate the center of the laser (lx, ly), depending on the center of the ship (cy, cy) and the direction of fire (dx, dy):

class Drone_Arc(Ship):
    # [...]

     def shoot(self, angle):
        if self.cooldown_counter == 0:
            radians = math.atan2((player.y+player.get_height()/2 - (self.y+self.get_height()/2)),(player.x+player.get_width()/2- (self.x+self.get_width()/2)))
            dx = math.cos(radians)
            dy = math.sin(radians)
            
            cx, cy =  self.x + self.get_height() / 2, self.y + self.get_width()/2
            lx, ly = cx + dx * (self.get_height()/2 + 11), cy + dy * (self.get_height()/2 + 11)
            
            laser_1 = Laser(lx, ly, 81, 22,dx, dy)
            laser_1.rotate_laser(angle)
            
            self.lasers.append(laser_1)
            self.cooldown_counter = 1


The collision detection no longer works because the second argument of pygame.mask.Mask.overlap is the offset between the top left edges of the masks:

def collide(obj1, obj2):
   offset_x = obj2.x - obj1.x
   offset_y = obj2.y - obj1.y
   return obj1.mask.overlap(obj2.mask, ((int(offset_x)), ((int(offset_y)))))

Since the attributes x and y of the laser do not store the top left edge but the pivot point (center point), the calculation of the offset is wrong.

However this can be fixed with ease. Just fix the calculation of the offset:

class Laser(pygame.sprite.Sprite):
    # [...]

        def collision(self, obj):
        offset_x = obj.x - self.rect.left
        offset_y = obj.y - self.rect.top
        return self.mask.overlap(obj.mask, ((int(offset_x)), ((int(offset_y)))))
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • 1
    Thanks a lot for your help,it works really nicely.Your answers are always really instructive and I must say that are the example you give in them are really helpful, to the point you probably recognized some of your own code in mine, for example the way I rotate the enemy ship. – Maximus Jan 04 '21 at 15:59
  • @Maximus Yes i recognized it. With this code, an object can be rotated around any pivot point. However, if you just want to rotate an object about its center, the code can be simplified using `pygame.Rect`. As a general note from me, to simplify your code, instead of storing a top left corner point of an object in attributes, store the pivot point. – Rabbid76 Jan 04 '21 at 16:54
  • The reason I did not use the pygame.Rect is that it cause problems with the collision detection that is used in my main code, as you can see here: https://gfycat.com/fr/ethicalelderlyanteater I've edited my code to include the feature – Maximus Jan 04 '21 at 20:31
  • I mean, i'm learning the language and the module, but I know it isn't really the best way to go about it, sorry – Maximus Jan 04 '21 at 20:43
  • Thanks a lot and sorry that you had to look at it a second time. I'll look at it more carefully for the next time instead of asking other people. – Maximus Jan 04 '21 at 21:11
  • 1
    @Maximus Why? Feel free to ask if you are struggling (after trying it yourself first). – Rabbid76 Jan 04 '21 at 21:14