Search code examples
pythonpygame

How to create sprites around a location?


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()

Solution

  • 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)))))