pythonpygame

Issue with Pygame shooter game


My game is experiencing a weird issue when the game starts a bullet is fired for no reason and the ammo that is displayed goes from 0/20 which is already wrong and goes to 0/0 without any user input???

I have included a minimal reimplementation of the original code only containing the problematic areas

import pygame as pg
import math

# Constants
WIN_LEN = 800
WIN_HEIGHT = 600
PLAYER_SIZE = 50
PLAYER_SPEED = 300
BULLET_SPEED = 700
BULLET_RADIUS = 5
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)

class Player(pg.sprite.Sprite):
    def __init__(self, pos):
        super().__init__()
        self.rect = pg.Rect(pos[0] - PLAYER_SIZE // 2, pos[1] - PLAYER_SIZE // 2, PLAYER_SIZE, PLAYER_SIZE)
        self.position = pg.Vector2(pos)
        self.angle = 0
        self.ammo = 20
        self.magazine = 10
        self.bullets = []

    def update(self, keys, dt):
        if keys[pg.K_SPACE]:
            self.fire_bullet()

        cursor_pos = pg.mouse.get_pos()
        dx, dy = cursor_pos[0] - self.position.x, cursor_pos[1] - self.position.y
        self.angle = math.degrees(math.atan2(dy, dx))
        self.rect.center = self.position

    def fire_bullet(self):
        if self.ammo > 0 and len(self.bullets) < self.magazine:
            self.ammo -= 1
            self.bullets.append(Bullet(self.position, self.angle))
        else:
            print("No ammo to fire.")

    def draw_ui(self, screen):
        font = pg.font.Font(None, 36)
        text = font.render(f"Ammo: {self.ammo}/{len(self.bullets)}", True, WHITE)
        screen.blit(text, (10, 10))


class Bullet(pg.sprite.Sprite):
    def __init__(self, position, angle):
        super().__init__()
        self.rect = pg.Rect(position[0], position[1], BULLET_RADIUS * 2, BULLET_RADIUS * 2)
        self.position = pg.Vector2(position)
        self.velocity = pg.Vector2(math.cos(math.radians(angle)), math.sin(math.radians(angle))) * BULLET_SPEED

    def update(self, dt):
        self.position += self.velocity * dt
        self.rect.center = self.position

    def draw(self, screen):
        pg.draw.circle(screen, WHITE, (int(self.position.x), int(self.position.y)), BULLET_RADIUS)


def main():
    pg.init()
    screen = pg.display.set_mode((WIN_LEN, WIN_HEIGHT))
    clock = pg.time.Clock()
    running = True

    player = Player((WIN_LEN // 2, WIN_HEIGHT // 2))
    all_sprites = pg.sprite.Group(player)

    while running:
        dt = clock.tick(60) / 1000

        for event in pg.event.get():
            if event.type == pg.QUIT:
                running = False

        keys = pg.key.get_pressed()
        all_sprites.update(keys, dt)

        screen.fill((0, 0, 0))  # Clear screen

        for bullet in player.bullets:
            bullet.update(dt)
            bullet.draw(screen)

        pg.draw.rect(screen, BLUE, player.rect)  # Draw player

        player.draw_ui(screen)

        pg.display.flip()

    pg.quit()

if __name__ == "__main__":
    main()

Thanks

What actually resulted at first was nothing and then came to something even worse


Solution

  • It is nothing weird. Program runs very fast and when you keep pressed key then it may run Player.update() many times and it may shoot many bullets.

    If you want one shot on one keypress then you should use even KEYDOWN (or KEYUP) because it generates only one event - and it doesn't matter how long you keep button pressed.

    (It can be also useful when you want to use mouse to click some element only once - e.g. press button on screen - and this may need event MOUSEBUTTONDOWN MOUSEBUTTONUP)

    Here I add function handle_event(event) in Player and I execute it in for event loop.

    class Player(pg.sprite.Sprite):
    
        # ... code ...
    
        def update(self, keys, dt):
            #if keys[pg.K_SPACE]:
            #    self.fire_bullet()
    
            cursor_pos = pg.mouse.get_pos()
            dx, dy = cursor_pos[0] - self.position.x, cursor_pos[1] - self.position.y
            self.angle = math.degrees(math.atan2(dy, dx))
            self.rect.center = self.position
    
        def handle_event(self, event):
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_SPACE:
                    self.fire_bullet()
    
    # ... code ...
    
        while running:
            dt = clock.tick(60) / 1000
    
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    running = False
                player.handle_event(event)  # <-- execute in event loop
    

    Full working code:

    import pygame as pg
    import math
    
    # Constants
    WIN_LEN = 800
    WIN_HEIGHT = 600
    PLAYER_SIZE = 50
    PLAYER_SPEED = 300
    BULLET_SPEED = 700
    BULLET_RADIUS = 5
    WHITE = (255, 255, 255)
    BLUE = (0, 0, 255)
    
    class Player(pg.sprite.Sprite):
        def __init__(self, pos):
            super().__init__()
            self.rect = pg.Rect(pos[0] - PLAYER_SIZE // 2, pos[1] - PLAYER_SIZE // 2, PLAYER_SIZE, PLAYER_SIZE)
            self.position = pg.Vector2(pos)
            self.angle = 0
            self.ammo = 20
            self.magazine = 10
            self.bullets = []
    
        def update(self, keys, dt):
            #if keys[pg.K_SPACE]:
            #    self.fire_bullet()
    
            cursor_pos = pg.mouse.get_pos()
            dx, dy = cursor_pos[0] - self.position.x, cursor_pos[1] - self.position.y
            self.angle = math.degrees(math.atan2(dy, dx))
            self.rect.center = self.position
    
        def handle_event(self, event):
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_SPACE:
                    self.fire_bullet()
    
        def fire_bullet(self):
            if self.ammo > 0 and len(self.bullets) < self.magazine:
                self.ammo -= 1
                self.bullets.append(Bullet(self.position, self.angle))
            else:
                print("No ammo to fire.")
    
        def draw_ui(self, screen):
            font = pg.font.Font(None, 36)
            text = font.render(f"Ammo: {self.ammo}/{len(self.bullets)}", True, WHITE)
            screen.blit(text, (10, 10))
    
    
    class Bullet(pg.sprite.Sprite):
        def __init__(self, position, angle):
            super().__init__()
            self.rect = pg.Rect(position[0], position[1], BULLET_RADIUS * 2, BULLET_RADIUS * 2)
            self.position = pg.Vector2(position)
            self.velocity = pg.Vector2(math.cos(math.radians(angle)), math.sin(math.radians(angle))) * BULLET_SPEED
    
        def update(self, dt):
            self.position += self.velocity * dt
            self.rect.center = self.position
    
        def draw(self, screen):
            pg.draw.circle(screen, WHITE, (int(self.position.x), int(self.position.y)), BULLET_RADIUS)
    
    
    def main():
        pg.init()
        screen = pg.display.set_mode((WIN_LEN, WIN_HEIGHT))
        clock = pg.time.Clock()
        running = True
    
        player = Player((WIN_LEN // 2, WIN_HEIGHT // 2))
        all_sprites = pg.sprite.Group(player)
    
        while running:
            dt = clock.tick(60) / 1000
    
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    running = False
                player.handle_event(event)
    
            keys = pg.key.get_pressed()
            all_sprites.update(keys, dt)
    
            screen.fill((0, 0, 0))  # Clear screen
    
            for bullet in player.bullets:
                bullet.update(dt)
                bullet.draw(screen)
    
            pg.draw.rect(screen, BLUE, player.rect)  # Draw player
    
            player.draw_ui(screen)
    
            pg.display.flip()
    
        pg.quit()
    
    if __name__ == "__main__":
        main()
    

    But some games allow to fire many bullets when you keep pressed key - and this may need to count time between shots to shoot less bullets in short time.

    class Player(pg.sprite.Sprite):
    
        def __init__(self, pos):
    
            # ... code ...
    
            self.bullet_delay = 500
            self.next_bullet = pg.time.get_ticks()
            
        def update(self, keys, dt):
            if keys[pg.K_SPACE]:
                # check if I can fire next bullet
                if self.next_bullet <= pg.time.get_ticks():
                    # calculate time for next bullet
                    self.next_bullet = pg.time.get_ticks() + self.bullet_delay
                    self.fire_bullet()
    

    It may need to add it to fire_bullet() and calculate self.next_bullet only if there was bullet in magazine.

    This method allows to change self.bullet_delay to have faster gun. "PowerUps".


    Full working code:

    import pygame as pg
    import math
    
    # Constants
    WIN_LEN = 800
    WIN_HEIGHT = 600
    PLAYER_SIZE = 50
    PLAYER_SPEED = 300
    BULLET_SPEED = 700
    BULLET_RADIUS = 5
    WHITE = (255, 255, 255)
    BLUE = (0, 0, 255)
    
    class Player(pg.sprite.Sprite):
    
        def __init__(self, pos):
            super().__init__()
            self.rect = pg.Rect(pos[0] - PLAYER_SIZE // 2, pos[1] - PLAYER_SIZE // 2, PLAYER_SIZE, PLAYER_SIZE)
            self.position = pg.Vector2(pos)
            self.angle = 0
            self.ammo = 20
            self.magazine = 10
            self.bullets = []
    
            self.bullet_delay = 500
            self.next_bullet = pg.time.get_ticks()
            
        def update(self, keys, dt):
            if keys[pg.K_SPACE]:
                if self.next_bullet <= pg.time.get_ticks():
                    self.next_bullet = pg.time.get_ticks() + self.bullet_delay
                    self.fire_bullet()
    
            cursor_pos = pg.mouse.get_pos()
            dx, dy = cursor_pos[0] - self.position.x, cursor_pos[1] - self.position.y
            self.angle = math.degrees(math.atan2(dy, dx))
            self.rect.center = self.position
    
        def fire_bullet(self):
            if self.ammo > 0 and len(self.bullets) < self.magazine:
                self.ammo -= 1
                self.bullets.append(Bullet(self.position, self.angle))
            else:
                print("No ammo to fire.")
    
        def draw_ui(self, screen):
            font = pg.font.Font(None, 36)
            text = font.render(f"Ammo: {self.ammo}/{len(self.bullets)}", True, WHITE)
            screen.blit(text, (10, 10))
    
    
    class Bullet(pg.sprite.Sprite):
        def __init__(self, position, angle):
            super().__init__()
            self.rect = pg.Rect(position[0], position[1], BULLET_RADIUS * 2, BULLET_RADIUS * 2)
            self.position = pg.Vector2(position)
            self.velocity = pg.Vector2(math.cos(math.radians(angle)), math.sin(math.radians(angle))) * BULLET_SPEED
    
        def update(self, dt):
            self.position += self.velocity * dt
            self.rect.center = self.position
    
        def draw(self, screen):
            pg.draw.circle(screen, WHITE, (int(self.position.x), int(self.position.y)), BULLET_RADIUS)
    
    
    def main():
        pg.init()
        screen = pg.display.set_mode((WIN_LEN, WIN_HEIGHT))
        clock = pg.time.Clock()
        running = True
    
        player = Player((WIN_LEN // 2, WIN_HEIGHT // 2))
        all_sprites = pg.sprite.Group(player)
    
        while running:
            dt = clock.tick(60) / 1000
    
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    running = False
                #player.handle_event(event)
    
            keys = pg.key.get_pressed()
            all_sprites.update(keys, dt)
    
            screen.fill((0, 0, 0))  # Clear screen
    
            for bullet in player.bullets:
                bullet.update(dt)
                bullet.draw(screen)
    
            pg.draw.rect(screen, BLUE, player.rect)  # Draw player
    
            player.draw_ui(screen)
    
            pg.display.flip()
    
        pg.quit()
    
    if __name__ == "__main__":
        main()