Reputation: 119
So I'm writing a game with pygame where a player runs away from zombie blocks and shoots them to kill them. I have the bullets shooting towards my mouse position which is what I want. However, the bullets seem to only be able to shoot from about 10 different axis' instead of anywhere in a 360 degree circle. How do I fix this to make it so the bullets always go straight to the mouse cursor instead of close to it?
-Here is my whole program so you can copy / paste it to try out the game. Notice how the bullets don't follow the mouse position exactly. How can this be fixed? The code for the bullets will be posted below the whole game code. WASD to move, mouse to aim and shoot, press 'P' to quit:
import pygame
import math
import random
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
ORANGE = (255, 119, 0)
ZOMBIE_GREEN = (122, 172, 34)
cursor_x = 100
cursor_y = 100
class Player(pygame.sprite.Sprite):
def __init__(self, color):
super().__init__()
# pygame.Surface will create a rectangle with the width and height given
# and the command below it tells it to fill it in with that color
self.image = pygame.Surface([15, 15])
self.image.fill(color)
self.rect = self.image.get_rect()
# This defines the starting position (x, y)
# of whatever sprite is passed through
self.rect.x = 600
self.rect.y = 300
# This is the current speed it will move when drawn
self.change_x = 0
self.change_y = 0
self.walls = None
# Defines how the player will move
def movement(self, x, y):
self.change_x += x
self.change_y += y
# Updates the information so the screen shows the player moving
def update(self):
self.rect.x += self.change_x
# Did this update cause us to hit a wall?
block_hit_list = pygame.sprite.spritecollide(self, self.walls, False)
for block in block_hit_list:
# If we are moving right, set our right side to the left side of
# the item we hit
if self.change_x > 0:
self.rect.right = block.rect.left
else:
# Otherwise if we are moving left, do the opposite.
self.rect.left = block.rect.right
self.rect.y += self.change_y
# Check and see if we hit anything
block_hit_list = pygame.sprite.spritecollide(self, self.walls, False)
for block in block_hit_list:
# Reset our position based on the top/bottom of the object.
if self.change_y > 0:
self.rect.bottom = block.rect.top
else:
self.rect.top = block.rect.bottom
class Enemy(pygame.sprite.Sprite):
def __init__(self, color):
super().__init__()
self.image = pygame.Surface([20, 20])
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.x = random.randrange(35, screen_width - 35)
self.rect.y = random.randrange(35, screen_height - 135)
self.change_x = 0
self.change_y = 0
self.walls = None
def movement(self, x, y):
self.change_x += x
self.change_y += y
class Wall(pygame.sprite.Sprite):
def __init__(self, color, x, y, width, height):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
class Cursor(pygame.sprite.Sprite):
def __init__(self, width, height):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(RED)
self.rect = self.image.get_rect()
self.walls = None
# This updates the cursor to move along with your
# mouse position (defined in control logic)
def update(self):
self.rect.x = cursor_x
self.rect.y = cursor_y
class Bullet(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface([8, 8])
self.image.fill(ORANGE)
self.rect = self.image.get_rect()
self.rect.x = player.rect.x + 4
self.rect.y = player.rect.y + 4
self.walls = None
self.change_x = 0
self.change_y = 0
def bullet_movement(self, cursor_pos_x, cursor_pos_y, player_pos_x, player_pos_y):
bullet_vec_x = cursor.rect.x - player.rect.x
bullet_vec_y = cursor.rect.y - player.rect.y
vec_length = math.sqrt(bullet_vec_x ** 2 + bullet_vec_y ** 2) # Normalizing the Vector
bullet_vec_x = (bullet_vec_x / vec_length) * 5 # These numbers determine how
bullet_vec_y = (bullet_vec_y / vec_length) * 5 # fast the bullets travel
self.change_x += bullet_vec_x
self.change_y += bullet_vec_y
def update(self):
self.rect.x += self.change_x
self.rect.y += self.change_y
pygame.init()
screen_size = pygame.display.Info()
#size = (900, 700)
#screen = pygame.display.set_mode(size)
size = (screen_size.current_w, screen_size.current_h)
screen = pygame.display.set_mode(
((screen_size.current_w, screen_size.current_h)),pygame.FULLSCREEN
)
screen_width = screen_size.current_w
screen_height = screen_size.current_h
pygame.display.set_caption("Zombie Shooter")
wall_list = pygame.sprite.Group()
sprites_list = pygame.sprite.Group()
bullet_list = pygame.sprite.Group()
all_sprites_list = pygame.sprite.Group()
# Walls are made here = (x_coord for where it starts,
# y_coord for where it starts, width of wall, height of wall)
# These walls are made with fullscreen dimentions, not any set dimentions
# Left
wall = Wall(BLUE, 0, 0, 10, screen_height)
wall_list.add(wall)
all_sprites_list.add(wall)
# Top
wall = Wall(BLUE, 0, 0, screen_width, 10)
wall_list.add(wall)
all_sprites_list.add(wall)
# Bottom
wall = Wall(BLUE, 0, screen_height - 10, screen_width, 10)
wall_list.add(wall)
all_sprites_list.add(wall)
# Right
wall = Wall(BLUE, screen_width - 10, 0, 10, screen_width)
wall_list.add(wall)
all_sprites_list.add(wall)
# HUD Border
wall = Wall(BLUE, 0, screen_height - 100, screen_width, 10)
wall_list.add(wall)
all_sprites_list.add(wall)
# This creates the actual player with the parameters set in ( ).
# However, we must add the player to the all_sprites_list
# so that it will actually be drawn to the screen with the draw command
# placed right after the screen.fill(BLACK) command.
player = Player(WHITE)
player.walls = wall_list
all_sprites_list.add(player)
cursor = Cursor(7, 7)
cursor.walls = wall_list
all_sprites_list.add(cursor)
zombie = Enemy(ZOMBIE_GREEN)
for i in range(10):
zombie = Enemy(ZOMBIE_GREEN)
all_sprites_list.add(zombie)
sprites_list.add(zombie)
bullet = Bullet()
done = False
clock = pygame.time.Clock()
pygame.mouse.set_visible(0)
# -------- Main Program Loop -----------
while not done:
# --- Main event loop ---
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_p:
done = True
# Keyboard controls. The numbers inside change the speed of the player
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_a:
player.movement(-4, 0)
elif event.key == pygame.K_d:
player.movement(4, 0)
elif event.key == pygame.K_w:
player.movement(0, -4)
elif event.key == pygame.K_s:
player.movement(0, 4)
elif event.type == pygame.KEYUP:
if event.key == pygame.K_a:
player.movement(4, 0)
elif event.key == pygame.K_d:
player.movement(-4, 0)
elif event.key == pygame.K_w:
player.movement(0, 4)
elif event.key == pygame.K_s:
player.movement(0, -4)
pos = pygame.mouse.get_pos()
cursor_x = pos[0]
cursor_y = pos[1]
if cursor_x <= 10:
cursor_x = 10
if cursor_x >= (screen_width - 17):
cursor_x = (screen_width - 17)
if cursor_y <= 10:
cursor_y = 10
if cursor_y >= (screen_height - 107):
cursor_y = (screen_height - 107)
elif event.type == pygame.MOUSEBUTTONDOWN:
bullet = Bullet()
all_sprites_list.add(bullet)
bullet_list.add(bullet)
bullet.bullet_movement(cursor.rect.x, cursor.rect.y, player.rect.x, player.rect.y)
all_sprites_list.update()
pygame.mouse.set_visible(0)
for bullet in bullet_list:
block_hit_list = pygame.sprite.spritecollide(bullet, sprites_list, True)
for i in block_hit_list:
bullet_list.remove(bullet)
all_sprites_list.remove(bullet)
for bullet in bullet_list:
block_hit_list = pygame.sprite.spritecollide(bullet, wall_list, False)
for i in block_hit_list:
bullet_list.remove(bullet)
all_sprites_list.remove(bullet)
# .update() will 'update' or change the screen with what
# we've told it to everytime we run throught the loop. Without
# this our player would not appear to move on the screen because
# we wouldn't be telling the screen to change the coordinates of the player.
cursor.update()
bullet_list.update()
screen.fill(BLACK)
all_sprites_list.draw(screen)
pygame.display.flip()
clock.tick(60)
pygame.quit()
Here's how the bullet vector's are calculated. Can this be edited to make the bullets more accurately go towards the red cursor position?
class Bullet(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface([8, 8])
self.image.fill(ORANGE)
self.rect = self.image.get_rect()
self.rect.x = player.rect.x + 4
self.rect.y = player.rect.y + 4
self.walls = None
self.change_x = 0
self.change_y = 0
def bullet_movement(self, cursor_pos_x, cursor_pos_y, player_pos_x, player_pos_y):
bullet_vec_x = cursor.rect.x - player.rect.x
bullet_vec_y = cursor.rect.y - player.rect.y
vec_length = math.sqrt(bullet_vec_x ** 2 + bullet_vec_y ** 2) # Normalizing the Vector
bullet_vec_x = (bullet_vec_x / vec_length) * 5 # These numbers determine how
bullet_vec_y = (bullet_vec_y / vec_length) * 5 # fast the bullets travel
self.change_x += bullet_vec_x
self.change_y += bullet_vec_y
def update(self):
self.rect.x += self.change_x
self.rect.y += self.change_y
Upvotes: 1
Views: 1499
Reputation: 104712
I suspect this is an integer math issue. At some angles, the velocities you're computing for the bullets require moving a fraction of a pixel in a given direction, but pygame.rect
only supports integer coordinates.
I'm not exactly sure how Pygame's types are implemented, but I suspect this means the fractional part of the updated position is discarded on each frame. Because many velocity vectors get rounded the same way, you end up with a more limited set of angles than you want.
A solution would be to maintain your own (floating point) position values. Your update code would add the velocity components to the floating point position values, then copy the updated values to the pygame.rect
coordinates. This way the rounding errors would not accumulate and you'd see the difference between different vectors.
class Bullet(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface([8, 8])
self.image.fill(ORANGE)
self.rect = self.image.get_rect()
self.pos_x = player.rect.x + 4 # Set up pos_x and pos_y here
self.pos_y = player.rect.y + 4 # rather than rect.x and rect.y
self.walls = None
self.change_x = 0
self.change_y = 0
def bullet_movement(self, cursor_pos_x, cursor_pos_y, player_pos_x, player_pos_y):
bullet_vec_x = cursor.rect.x - player.rect.x
bullet_vec_y = cursor.rect.y - player.rect.y
vec_length = math.sqrt(bullet_vec_x ** 2 + bullet_vec_y ** 2)
bullet_vec_x = (bullet_vec_x / vec_length) * 5
bullet_vec_y = (bullet_vec_y / vec_length) * 5
self.change_x += bullet_vec_x
self.change_y += bullet_vec_y
def update(self):
self.pos_x += self.change_x # Update pos_x and pos_y. They will become floats
self.pos_y += self.change_y # which will let them maintain sub-pixel accuracy.
self.rect.x = self.pos_x # Copy the pos values into the rect, where they will be
self.rect.y = self.pos_y # rounded off. That's OK since we never read them back.
There are some odds areas of this code, such as the fact that you don't use the arguments to the bullet_movement
function at all, but rather just look up the global values player
and cursor
(you also look up player
in the __init__
method). I've not modified those though, but you may want to, once you get the rounding issue under control. I'd also consider merging the code from bullet_movement
into __init__
, since there's no situation where you'd want to create a bullet without setting its movement up too.
Upvotes: 2