Paras Sharma
Paras Sharma

Reputation: 25

Creating a Auto targeting bullet

I have been creating a typical type of space invader game using Python and pygame sprites. I have done most of the things and it's working fine. But actually in my game, the bullet always shoots straight, but I want it to target the enemy and always shoot where the enemy is.

In my player update I am just doing that whenever space is hit, it fires the bullet.

if (keys[pygame.K_SPACE]):
    self.fire()

This below is my fire method which is just calling the Handgun (which is my bullet class):

def fire(self):
    now = pygame.time.get_ticks()
    self.shoot_delay = 600
    self.shot_position = handguns.rect.x - enemy.rect.x
    print (self.shot_position)
    if (now - self.last_shot > self.shoot_delay):
        self.last_shot = now
        shot = HandGun(self.rect.centerx, self.rect.top)
        Every_Sprite.add(shot)
        handgun.add(shot)

This below is my enemy class where the position is just randomised:

class Enemy_Agent(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join(img_folder, "ship2.png")).convert()
        self.image.set_colorkey(WHITE)
        self.rect =self.image.get_rect()
        self.rect.x = random.randrange(width - self.rect.width)
        self.rect.y = random.randrange(-100, -40)

        self.speed = random.randrange(1,8)
        self.speedx = random.randrange(-3,3)
    def update(self):
        if (self.rect.right > width):
            self.rect.right = width
        if (self.rect.left < 0):
            self.rect.left = 0

        self.rect.y += self.speed
        #print("pos: ", self.rect.y)
        self.rect.x += self.speedx
        if(self.rect.top > Height - 10 or self.rect.left < -30 or     self.rect.right > width + 20):
            self.rect.x = random.randrange(width - self.rect.width)
            self.rect.y = random.randrange(-100, -40)
            self.speed = random.randrange(1, 8)

And, last this my HandGun class. If anyone can help me and advise me to make the bullet target the enemy it would be a great help.

class HandGun(pygame.sprite.Sprite):
    def __init__(self, cx, cy):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join(img_folder, "bullet.png")).convert()
        self.image.set_colorkey(WHITE)
        self.rect = self.image.get_rect()
        self.rect.bottom = cy
        self.rect.centerx =cx
        self.speedy = -1
        self.speedx = None
    def update(self):
        self.rect.y += self.speedy + enemy.rect.centerx
        if(self.rect.bottom < 0):
            self.kill()
        pass

Upvotes: 1

Views: 1371

Answers (1)

skrx
skrx

Reputation: 20438

Here's a minimal example in which I just shoot bullets from the mouse position towards a target.

First we need a vector that points to the target (called direction here), therefore we subtract the mouse position from the target position.

We use the angle of the direction vector (which you can get with the as_polar method (polar coordinates)) to rotate the bullet.

To get the velocity vector, we can normalize the direction and multiply it by a scalar to scale it to the desired length (i.e. the speed).

import pygame as pg
from pygame.math import Vector2


BACKGROUND_COLOR = pg.Color(30, 30, 50)
BLUE = pg.Color('dodgerblue1')
LIME = pg.Color(192, 255, 0)


class Bullet(pg.sprite.Sprite):
    """ This class represents the bullet. """

    def __init__(self, pos, target, screen_rect):
        """Take the pos, direction and angle of the player."""
        super().__init__()
        self.image = pg.Surface((16, 10), pg.SRCALPHA)
        pg.draw.polygon(self.image, LIME, ((0, 0), (16, 5), (0, 10)))
        # The `pos` parameter is the center of the bullet.rect.
        self.rect = self.image.get_rect(center=pos)
        self.position = Vector2(pos)  # The position of the bullet.

        # This vector points from the mouse pos to the target.
        direction = target - pos
        # The polar coordinates of the direction vector.
        radius, angle = direction.as_polar()
        # Rotate the image by the negative angle (because the y-axis is flipped).
        self.image = pg.transform.rotozoom(self.image, -angle, 1)
        # The velocity is the normalized direction vector scaled to the desired length.
        self.velocity = direction.normalize() * 11
        self.screen_rect = screen_rect

    def update(self):
        """Move the bullet."""
        self.position += self.velocity  # Update the position vector.
        self.rect.center = self.position  # And the rect.

        # Remove the bullet when it leaves the screen.
        if not self.screen_rect.contains(self.rect):
            self.kill()


def main():
    pg.init()
    screen = pg.display.set_mode((800, 600))
    screen_rect = screen.get_rect()
    clock = pg.time.Clock()

    all_sprites = pg.sprite.Group()
    bullet_group = pg.sprite.Group()

    target = Vector2(400, 300)

    done = False
    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            elif event.type == pg.MOUSEBUTTONDOWN:
                # Shoot a bullet. Pass the start position (in this
                # case the mouse position) and the direction vector.
                bullet = Bullet(event.pos, target, screen_rect)
                all_sprites.add(bullet)
                bullet_group.add(bullet)

        all_sprites.update()

        screen.fill(BACKGROUND_COLOR)
        all_sprites.draw(screen)
        pg.draw.rect(screen, BLUE, (target, (3, 3)), 1)
        pg.display.flip()
        clock.tick(60)


if __name__ == '__main__':
    main()
    pg.quit()

If you're not familiar with vectors, you can use trigonometry as well.

If you want a homing missile, you have to pass the current target position to the bullet and recompute the direction and velocity each frame.


To aim at a moving target, you need to calculate the future position where the projectile will hit the target. You can do that with a quadratic equation. I'm using Jeffrey Hantin's solution from this answer here. You have to pass the start position of the bullet, its speed and the target position and velocity to the intercept function and then solve the quadratic equation. It will return the position vector where the bullet and the target meet. Then just shoot at this point instead of the current target point (you can still use the same code in the Bullet class).

import math

import pygame as pg
from pygame.math import Vector2


BACKGROUND_COLOR = pg.Color(30, 30, 50)
BLUE = pg.Color('dodgerblue1')
LIME = pg.Color(192, 255, 0)


class Bullet(pg.sprite.Sprite):
    """ This class represents the bullet. """

    def __init__(self, pos, target, screen_rect):
        """Take the pos, direction and angle of the player."""
        super().__init__()
        self.image = pg.Surface((16, 10), pg.SRCALPHA)
        pg.draw.polygon(self.image, LIME, ((0, 0), (16, 5), (0, 10)))
        # The `pos` parameter is the center of the bullet.rect.
        self.rect = self.image.get_rect(center=pos)
        self.position = Vector2(pos)  # The position of the bullet.

        # This vector points from the mouse pos to the target.
        direction = target - pos
        # The polar coordinates of the direction vector.
        radius, angle = direction.as_polar()
        # Rotate the image by the negative angle (because the y-axis is flipped).
        self.image = pg.transform.rotozoom(self.image, -angle, 1)
        # The velocity is the normalized direction vector scaled to the desired length.
        self.velocity = direction.normalize() * 11
        self.screen_rect = screen_rect

    def update(self):
        """Move the bullet."""
        self.position += self.velocity  # Update the position vector.
        self.rect.center = self.position  # And the rect.

        # Remove the bullet when it leaves the screen.
        if not self.screen_rect.contains(self.rect):
            self.kill()


def intercept(position, bullet_speed, target, target_velocity):
    a = target_velocity.x**2 + target_velocity.y**2 - bullet_speed**2
    b = 2 * (target_velocity.x * (target.x - position.x) + target_velocity.y * (target.y - position.y))
    c = (target.x - position.x)**2 + (target.y - position.y)**2

    discriminant = b*b - 4*a*c
    if discriminant < 0:
        print("Target can't be reached.")
        return None
    else:
        t1 = (-b + math.sqrt(discriminant)) / (2*a)
        t2 = (-b - math.sqrt(discriminant)) / (2*a)
        t = max(t1, t2)
        x = target_velocity.x * t + target.x
        y = target_velocity.y * t + target.y
        return Vector2(x, y)


def main():
    pg.init()
    screen = pg.display.set_mode((800, 600))
    screen_rect = screen.get_rect()
    clock = pg.time.Clock()

    all_sprites = pg.sprite.Group()
    bullet_group = pg.sprite.Group()

    target = Vector2(50, 300)
    target_velocity = Vector2(4, 3)

    done = False
    while not done:
        for event in pg.event.get():
            if event.type == pg.QUIT:
                done = True
            elif event.type == pg.MOUSEBUTTONDOWN:
                target_vector = intercept(Vector2(event.pos), 11, target, target_velocity)
                # Shoot a bullet. Pass the start position (in this
                # case the mouse position) and the target position vector.
                if target_vector is not None:  # Shoots only if the target can be reached.
                    bullet = Bullet(event.pos, target_vector, screen_rect)
                    all_sprites.add(bullet)
                    bullet_group.add(bullet)

        target += target_velocity
        if target.x >= screen_rect.right or target.x < 0:
            target_velocity.x *= -1
        if target.y >= screen_rect.bottom or target.y < 0:
            target_velocity.y *= -1

        all_sprites.update()

        screen.fill(BACKGROUND_COLOR)
        all_sprites.draw(screen)
        pg.draw.rect(screen, BLUE, (target, (5, 5)))
        pg.display.flip()
        clock.tick(60)


if __name__ == '__main__':
    main()
    pg.quit()

Actually, it would be better to use broofa's solution because it takes some special cases into account.

def intercept(position, bullet_speed, target, target_velocity):
    tx, ty = target - position
    tvx, tvy = target_velocity
    v = bullet_speed
    dstx, dsty = target

    a = tvx*tvx + tvy*tvy - v*v
    b = 2 * (tvx*tx + tvy*ty)
    c = tx*tx + ty*ty

    ts = quad(a, b, c)

    sol = None

    if ts:
        t0 = ts[0]
        t1 = ts[1]
        t = min(t0, t1)
        if t < 0:
            t = max(t0, t1)
        if t > 0:
            sol = Vector2(dstx + tvx * t,
                          dsty + tvy * t)

    return sol


def quad(a, b, c):
    sol = None
    if abs(a) < 1e-6:
        if abs(b) < 1e-6:
            sol = [0, 0] if abs(c) < 1e-6 else None
        else:
            sol = [-c/b, -c/b]
    else:
        disc = b*b - 4*a*c
        if disc >= 0:
            disc = math.sqrt(disc)
            a = 2*a
            sol = [(-b-disc)/a, (-b+disc)/a]

    return sol

Upvotes: 3

Related Questions