There seems to be a weird offset when the bullets are shot from the ship in certain angles while the ship is moving. Also if the ship is shooting in the same direction is heading then the bullet speed is lower.
I tried to work with some SO answers and that's what I came up with:
import sys
import pygame
from pygame.locals import *
vec = pygame.math.Vector2
pygame.init()
FPS = 60
fps_clock = pygame.time.Clock()
WIDTH = 800
HEIGHT = 800
DISPLAY = pygame.display.set_mode((WIDTH, HEIGHT))
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)
MAX_SPEED = 7
class Player(pygame.sprite.Sprite):
"""This class represents the Player."""
def __init__(self):
"""Set up the player on creation."""
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((70, 50), pygame.SRCALPHA)
pygame.draw.polygon(self.image, (50, 120, 180), ((35, 0), (0, 35), (70, 35)))
self.original_image = self.image
self.position = vec(WIDTH / 2, HEIGHT / 2)
self.rect = self.image.get_rect(center=self.position)
self.vel = vec(0, 0)
self.acceleration = vec(0, -0.2) # The acceleration vec points upwards.
self.angle_speed = 0
self.angle = 0
def update(self):
"""Update the player's position."""
keys = pygame.key.get_pressed()
if keys[K_LEFT]:
self.angle_speed = -2
player.rotate()
if keys[K_RIGHT]:
self.angle_speed = 2
player.rotate()
# If up is pressed, accelerate the ship by
# adding the acceleration to the velocity vector.
if keys[K_UP]:
self.vel += self.acceleration
if keys[K_SPACE]:
player.shoot()
# max speed
if self.vel.length() > MAX_SPEED:
self.vel.scale_to_length(MAX_SPEED)
self.position += self.vel
self.rect.center = self.position
def rotate(self):
# rotate the acceleration vector
self.acceleration.rotate_ip(self.angle_speed)
self.angle += self.angle_speed
if self.angle > 360:
self.angle -= 360
elif self.angle < 0:
self.angle += 360
self.image = pygame.transform.rotate(self.original_image, -self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
def wrap_around_screen(self):
"""Wrap around screen."""
if self.position.x > WIDTH:
self.position.x = 0
if self.position.x < 0:
self.position.x = WIDTH
if self.position.y <= 0:
self.position.y = HEIGHT
if self.position.y > HEIGHT:
self.position.y = 0
def shoot(self):
# create and add missile object to the group
missile = Missile(self.rect.center, self.acceleration, player.acceleration.as_polar()[1])
all_sprites.add(missile)
missiles.add(missile)
class Missile(pygame.sprite.Sprite):
"""This class represents the bullet.
A missile launched by the player's ship.
"""
def __init__(self, position, direction, angle):
"""Initialize missile sprite.
Take the position, direction and angle of the player.
"""
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface([4, 10], pygame.SRCALPHA)
self.image.fill(BLUE)
# Rotate the image by the player.angle
self.image = pygame.transform.rotozoom(self.image, angle, 1)
# Pass the center of the player as the center of the bullet.rect.
self.rect = self.image.get_rect(center=position)
self.position = vec(position) # The position vector.
self.velocity = direction * 50 # Multiply by desired speed.
def update(self):
"""Move the bullet."""
self.position += self.velocity # Update the position vector.
self.rect.center = self.position # And the rect.
if self.rect.x < 0 or self.rect.x > WIDTH or self.rect.y < 0 or self.rect.y > HEIGHT:
self.kill()
all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)
missiles = pygame.sprite.Group()
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
player.wrap_around_screen()
all_sprites.update()
DISPLAY.fill(BLACK)
all_sprites.draw(DISPLAY)
pygame.display.set_caption('angle {:.1f} accel {} accel angle {:.1f}'.format(
player.angle, player.acceleration, player.acceleration.as_polar()[1]))
pygame.display.update()
fps_clock.tick(FPS)**
Any help would be appreciated
You almost certainly want to have your missiles start with the velocity of the launching ship (in addition to some speed in the direction the ship is facing).
You might replace the direction argument to Missile.__init__ with a velocity parameter that you save directly as self.velocity (with no multiplication). The Player.shoot method can then be modified to pass an appropriate value that takes both the ship's orientation and velocity into account:
def shoot(self):
# create and add missile object to the group
missile = Missile(self.rect.center,
self.velocity + 50 * self.acceleration, # new value here!
player.acceleration.as_polar()[1])
all_sprites.add(missile)
missiles.add(missile)
You might want to use a smaller multiple of ship's the acceleration vector, rather than the 50 I copied from your current code, since it won't be the only component of the missile's velocity any more.
The only problem there is that .rotozoom uses the angles in counter-clockwise, while the .as_polar vector method returns the angle in clockwise direction (one might expect .as_polar to yield an anti-clockwise angle, given what was learned in past math classes, but the Y axis on Surfaces points downwards, while math classes usually had Y pointing upwards).
TL;DR: You just have to invert the angle passed in the call to rotozoom:
self.image = pygame.transform.rotozoom(self.image, -angle, 1)
You probably will also want to swap the width X height of your missile:
self.image = pygame.Surface([10, 4], pygame.SRCALPHA)
Related
This question already has an answer here:
Sometimes the ball doesn't bounce off the paddle in pong game
(1 answer)
Closed 1 year ago.
I am currently trying to create a pong game but for some reason, the ball would not bounce off of the rectangles and I am not sure where I have made a mistake even though I followed a video about rectangle collision online. Please let me know where I made a mistake and how I can improve my code. Thanks!
import pygame, sys, random, time
from pygame.time import Clock
pygame.init()
#colours
black = ( 0, 0, 0)
white = ( 255, 255, 255)
green = ( 0, 255, 0)
red = ( 255, 0, 0)
# Display
screen_width = 1000
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("2-player Pong")
# Rectangles
class Rectangle():
def __init__(self, screen, x):
self.screen = screen
self.screen_rect= screen.get_rect()
self.x = x
self.y = 250
self.width = 30
self.height = 100
self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
self.colour = black
self.velocity = 5
def draw_rectangle(self):
pygame.draw.rect(self.screen, self.colour, self.rect)
rect1 = Rectangle(screen, 50)
rect2 = Rectangle(screen, 920)
class Ball():
def __init__(self, colour):
self.screen = screen
self.colour = colour
self.width = 20
self.height = 20
self.x = screen_width//2 - 25
self.y = screen_height//2 - 25
self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
self.possible_velocities_x = [-4, 4]
self.possible_velocities_y = [-2, 2]
self.velocity = [random.choice(self.possible_velocities_x), random.choice(self.possible_velocities_y)]
def draw_ball(self):
pygame.draw.rect(self.screen, self.colour, self.rect)
def move_ball(self):
global rect1, rect2
self.rect.x += self.velocity[0]
self.rect.y += self.velocity[1]
# Collision with Screen
if self.rect.top <= 10 or self.rect.bottom >= screen_height - 10:
self.velocity[1] *= -1
# Collision with Rectangles
if self.rect.colliderect(rect1) or self.rect.colliderect(rect2):
if self.rect.left - rect1.rect.right == 0:
self.possible_velocities_x *= -1
if self.rect.right - rect2.rect.left == 0:
self.possible_velocities_x *= -1
clock = pygame.time.Clock()
ball = Ball(white)
pong = True
while pong:
pygame.time.delay(10)
clock.tick(100)
# Watch for keyboard and mouse events.
for event in pygame.event.get():
if event.type == pygame.QUIT:
pong = False
keys = pygame.key.get_pressed()
if keys[pygame.K_UP] and rect2.rect.y >= 0:
rect2.rect.y -= rect2.velocity
if keys[pygame.K_DOWN] and rect2.rect.y <= 600 - rect2.rect.height:
rect2.rect.y += rect2.velocity
if keys[pygame.K_w] and rect1.rect.y >= 0:
rect1.rect.y -= rect1.velocity
if keys[pygame.K_s] and rect1.rect.y <= 600 - rect1.rect.height:
rect1.rect.y += rect1.velocity
screen.fill(green)
rect1.draw_rectangle()
rect2.draw_rectangle()
ball.draw_ball()
ball.move_ball()
# pygame.draw.rect(screen, black, (rect_x2, rect_y2, rect_width2, rect_height2))
pygame.display.update() # Make the most recently drawn screen visible.
pygame.quit()
You must change self.velocity[0] when the ball touches the paddle. Since the movement of the ball is more than 1 pixel per frame, the ball does not exactly touch the paddle. This mans the condition self.rect.left - rect1.rect.right == 0 and self.rect.right - rect2.rect.left == 0 will not be fulfilled. If the movement in x direction is negative, the new movement needs to be positive (abs(self.velocity[0])). If the movement in x direction is positive, the new movement needs to be negative (-abs(self.velocity[0])):
class Ball():
# [...]
def move_ball(self):
self.rect.x += self.velocity[0]
self.rect.y += self.velocity[1]
# Collision with Screen
if self.rect.top <= 10 or self.rect.bottom >= screen_height - 10:
self.velocity[1] *= -1
# Collision with Rectangles
if self.rect.colliderect(rect1) or self.rect.colliderect(rect2):
if self.velocity[0] < 0:
self.velocity[0] = abs(self.velocity[0])
else:
self.velocity[0] = -abs(self.velocity[0])
See also Sometimes the ball doesn't bounce off the paddle in pong game
I wanted to try something new and found this tutorial on the web. I used it and tweaked it a little. My goal is to create a zelda clone, meaning my game will be Tile-Based
Now for the problem.
All my sprites are not in 1:1 Ratio. The rect's size I created is TILESIZE * TILESIZE (=64 height, 64 width).
Now the rect sticks to the top:
but I need it to stick to the bottom center of my sprite:
for collision detection.
I tried numerous ways and asked my workmates, but they can't help me either.
class Player(pg.sprite.Sprite):
def __init__(self, game, x, y):
self.groups = game.all_sprites
pg.sprite.Sprite.__init__(self, self.groups)
self.images = []
def addImage(image):
self.images.append(load_image(path.join(img_folder, "zelda_green", image)))
print(self.images[-1].get_size())
scale = TILESIZE/self.images[-1].get_width()
#print(tuple([scale*x for x in self.images[-1].get_size()]))
print(scale)
self.images[-1] = pg.transform.scale(self.images[-1], tuple([int(scale*x) for x in self.images[-1].get_size()]))
addImage("walk1.png")
addImage("walk2.png")
addImage("walk3.png")
addImage("walk4.png")
addImage("walk5.png")
addImage("walk6.png")
addImage("walk7.png")
addImage("walk8.png")
addImage("walk9.png")
addImage("walk10.png")
self.index = 0
self.image = self.images[self.index]
self.rect = pg.Rect(0, 0, TILESIZE, TILESIZE)
self.game = game
self.vel = vec(0,0)
self.pos = vec(x,y) * TILESIZE
self.vx, self.vy = 0,0
self.x = x * TILESIZE
self.y = y * TILESIZE
def get_keys(self):
self.vel = vec(0,0)
keys = pg.key.get_pressed()
if keys[pg.K_LEFT] or keys[pg.K_a]:
self.vel.x = -PLAYER_SPEED
if keys[pg.K_RIGHT] or keys[pg.K_d]:
self.vel.x = PLAYER_SPEED
if keys[pg.K_UP] or keys[pg.K_w]:
self.vel.y = -PLAYER_SPEED
if keys[pg.K_DOWN] or keys[pg.K_s]:
self.vel.y = PLAYER_SPEED
if self.vel.x != 0 and self.vy != 0:
self.vel.x *= 0.7071
self.vel.y *= 0.7071
def move(self, dx=0, dy=0):
if not self.collide_with_walls(dx, dy):
self.x += dx
self.y += dy
def collide_with_walls(self, dir):
if dir == 'x':
hits = pg.sprite.spritecollide(self, self.game.walls, False)
if hits:
if self.vel.x > 0:
self.pos.x = hits[0].rect.left - self.rect.width
if self.vel.x < 0:
self.pos.x = hits[0].rect.right
self.vel.x = 0
self.rect.x = self.pos.x
if dir == 'y':
hits = pg.sprite.spritecollide(self, self.game.walls, False)
if hits:
if self.vel.y > 0:
self.pos.y = hits[0].rect.top - self.rect.height
if self.vel.y < 0:
self.pos.y = hits[0].rect.bottom
self.vel.y = 0
self.rect.y = self.pos.y
def update(self):
global initTime
frameTime = time.time() - initTime
if frameTime >= 0.075:
initTime = time.time()
self.index += 1
if self.index >= len(self.images):
self.index = 0
self.image = self.images[self.index]
self.get_keys()
self.pos += self.vel * self.game.dt
self.rect.x = self.pos.x
self.collide_with_walls('x')
self.rect.y = self.pos.y
self.collide_with_walls('y')
You can seperate the Rect that is used for drawing from the actual collision detection. If you look at the pygame.sprite.spritecollide() function, you can see a parameter called collided:
The collided argument is a callback function used to calculate if two sprites are colliding. it should take two sprites as values, and return a bool value indicating if they are colliding. If collided is not passed, all sprites must have a "rect" value, which is a rectangle of the sprite area, which will be used to calculate the collision.
Pygame already ships with several functions that you can use for collision detection:
collide_rect, collide_rect_ratio, collide_circle,
collide_circle_ratio, collide_mask
collide_rect is the default.
You can use collide_mask for pixel perfect collision, or you can provide your own function.
If you want to just "move" the Rect for collision detection, like you said in your comment, you could give your sprites a second Rect that's at the bottom of your image, something like this:
class Sprite(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((64, 80))
self.image.fill((0, 200, 200))
self.rect = pygame.Rect(0, 0, 64, 64)
# some drawing for demonstration
pygame.draw.rect(self.image, (255, 0, 0), self.rect, 2)
self.collision_rect = self.rect.copy()
self.collision_rect.bottom = self.image.get_rect().bottom
pygame.draw.rect(self.image, (255, 255, 0), self.collision_rect, 2)
then create a function to be passed as collided argument that uses that new collision_rect attribute:
def collide_collision_rect(left, right):
return left.collision_rect.colliderect(right.collision_rect)
and when calling spritecollide, pass the function as last argument:
pg.sprite.spritecollide(self, self.game.walls, False, collide_collision_rect)
You have to make sure all your sprites have a collision_rect attribute, or first check if the passed sprites have a collision_rect attribute and fall back to rect if they don't. Also, when you change the rect attribute, make sure to update the collision_rect attribute as well.
There are of course other ways to do this; you could also calculate how much bigger the image is than the rect, store that in the sprite, and in the new collided callback simply move the sprite's rect by that amount. Something like this:
class Sprite(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((64, 80))
self.image.fill((0, 200, 200))
self.rect = pygame.Rect(0, 0, 64, 64)
pygame.draw.rect(self.image, (255, 0, 0), self.rect, 2)
self.diff = self.image.get_rect().height - self.rect.height
def collide_collision_rect(left, right):
return left.rect.move(0, left.diff).colliderect(right.rect.move(0, right.diff))
I am trying to make an asteroid game and was wondering how to rotate the player clock wise or counter clock wise when the right or left keys have been pressed, and then when the up key is pressed the player should move forward.
class Player(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.transform.scale(player_img, (50, 38))
self.image.set_colorkey(BLACK)
self.rect = self.image.get_rect()
self.radius = 20
# pygame.draw.circle(self.image, RED, self.rect.center, self.radius)
self.rect.centerx = WIDTH / 2
self.rect.bottom = HEIGHT - 10
self.speedx = 0
self.speedy = 0
self.shield = 100
self.shoot_delay = 250
self.last_shot = pygame.time.get_ticks()
self.lives = 3
def update(self):
self.speedx = 0
keystate = pygame.key.get_pressed()
if keystate[pygame.K_LEFT]:
self.speedx = -8
if keystate[pygame.K_RIGHT]:
self.speedx = 8
if keystate[pygame.K_DOWN]:
self.speedy = 8
if keystate[pygame.K_UP]:
self.speedy = -8
if keystate[pygame.K_SPACE]:
self.shoot()
self.rect.x += self.speedx
self.rect.y += self.speedy
if self.rect.right > WIDTH:
self.rect.right = WIDTH
if self.rect.left < 0:
self.rect.left = 0
def shoot(self):
now = pygame.time.get_ticks()
if now - self.last_shot > self.shoot_delay:
self.last_shot = now
bullet = Bullet(self.rect.centerx, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
def hide(self):
# hide player temporarily
self.hidden = True
self.hide_timer = pygame.time.get_ticks()
self.rect.center = (WIDTH / 2, HEIGHT + 200)````
You can find below a working example (just rename the loaded image), I've kept only the essential code for movement and rotation.
import sys
import math
import pygame
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.player_img = pygame.image.load("yourimage.png").convert()
self.image = self.player_img
self.rect = self.image.get_rect()
self.rect.move_ip(x, y)
self.current_direction = 0 #0 degree == up
self.speed = 10
def update(self):
self.speedx = 0
keystate = pygame.key.get_pressed()
prev_center = self.rect.center
if keystate[pygame.K_LEFT]:
self.current_direction += 10
if keystate[pygame.K_RIGHT]:
self.current_direction -= 10
if keystate[pygame.K_DOWN]:
self.rect.x += self.speed * math.sin(math.radians(self.current_direction))
self.rect.y += self.speed * math.cos(math.radians(self.current_direction))
if keystate[pygame.K_UP]:
self.rect.x -= self.speed * math.sin(math.radians(self.current_direction))
self.rect.y -= self.speed * math.cos(math.radians(self.current_direction))
if keystate[pygame.K_LEFT] or keystate[pygame.K_RIGHT]:
self.image = pygame.transform.rotate(self.player_img, self.current_direction)
self.rect = self.image.get_rect()
self.rect.center = prev_center
pygame.init()
screen = pygame.display.set_mode((500, 500))
player = Player(200, 200)
clock = pygame.time.Clock()
while True:
screen.fill((0, 0, 0), player.rect)
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
player.update()
screen.blit(player.image, player.rect)
pygame.display.update()
clock.tick(50)
Since you want rotation, you do not need to assing speed directly to x and y but you need to calculate it according to the direction the player is facing.
The basic idea is to use transform.rotate. Keep track of the rotation angle (I called it current_direction), add / subcract a fixed amount (the rotation speed, if you wish) to this angle when LEFT or RIGTH keys are pressed, and then rotate the original image. Since rotation scales the image, I also keep track of the center of the rect, save the new rect from the iamge and and reassing the previous center to the rect.center after rotation, so that the image remains centered on rotation.
When UP or DOWN keys are pressed, you need to decompose the velocity on x and y axes using trigonometry and move the rect attribute coordinates.
I am in the process of making a 'main character' which jumps around and can jump onto boxes etc using pygame. I have created the Player class, the Level class and the Box class. The spritecollide function never returns true, and I am unsure where I am going wrong.
I used spritecollideany and groupcollide to see if that helps (it didn't)
I have checked as best I can, and as far as I can tell the boxes are making their way into Level.box_list as a sprite Group, and both the boxes and the players are Sprites.
I have thoroughly read the documentation and feel as though there must be some key concept I am missing or misusing.
I greatly appreciate any help that you are able to offer. Thanks!
movementprototype.py
import random, pygame, sys
import math
from pygame.locals import *
from levelprototype import *
FPS = 60 # frames per second, the general speed of the program
WINDOWWIDTH = 640 # size of window's width in pixels
WINDOWHEIGHT = 480 # size of windows' height in pixels
# R G B
GRAY = (100, 100, 100)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
GREEN = ( 0, 255, 0)
BLUE = ( 0, 0, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 128, 0)
BLACK = ( 0, 0, 0)
BGCOLOR = WHITE
def main():
#set up all relevant variables
global FPSCLOCK, DISPLAYSURF, player, box_list
pygame.init()
FPSCLOCK = pygame.time.Clock()
#the length, in ms, of one frame
tick = 1000/FPS
#create window
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
mousex = 0 # used to store x coordinate of mouse event
mousey = 0 # used to store y coordinate of mouse event
pygame.display.set_caption('Movement test environment')
#create the player object using Player() class, add to all_sprites group.
all_sprites = pygame.sprite.Group()
player = Player()
all_sprites.add(player)
#create the level object
level = Level()
while True: #main game loop
#fill with background colour
DISPLAYSURF.fill(BGCOLOR)
# Update items in the level
level.update()
level.draw(DISPLAYSURF)
#call the sprite update and draw methods.
all_sprites.update()
#check if the player has clicked the X or pressed escape.
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
[irrelevant code]
#update the screen
pygame.display.update()
FPSCLOCK.tick(FPS)
class Player(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
#.draw() called in main() requires that each Sprite have a Surface.image
#and a Surface.rect.
self.image = pygame.Surface((20,50))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
#attributes
self.velX = 0
self.velY = 0
self.x = 0
self.y = 0
self.gravity = 0.6
self.maxVelocity = 5
self.xResistance = 0.5
self.isJump = 0
self.isDash = 0
def update(self):
#check which keys are pressed and add velocity.
keys = pygame.key.get_pressed()
if (keys[K_d]) and self.velX < self.maxVelocity:
self.velX += 2.5
if (keys[K_a]) and self.velX > -self.maxVelocity:
self.velX -= 2.5
if (keys[K_w]) and self.isJump == 0:
self.velY -= 10
self.isJump = 1
if (keys[K_SPACE]) and self.isDash == 0:
#at the moment, can only dash once. add timer reset.
self.isDash = 1
if self.velX > 0:
self.x += 20
else:
self.x -= 20
#add gravity
self.velY += self.gravity
#add X resistance
if self.velX > 0.05:
self.velX -= self.xResistance
elif self.velX < -0.05:
self.velX += self.xResistance
#if velocity is really close to 0, make it 0, to stop xResistance moving it the other way.
if self.velX < 0.15 and self.velX > -0.15:
self.velX = 0
self.checkCollision()
#update position with calculated velocity
self.x += self.velX
self.y += self.velY
#call the draw function, below, to blit the sprite onto screen.
self.draw(DISPLAYSURF)
def checkCollision(self):
level = Level().box_list
for collide in pygame.sprite.spritecollide(self, level, False):
print("Collision")
#no matter what I put here, a collision never seems to be identified!!
def draw(self, DISPLAYSURF):
DISPLAYSURF.blit(self.image, (self.x, self.y))
if __name__ == '__main__':
main()
levelprototype.py
import pygame, sys
from pygame.locals import*
GREEN = ( 0, 255, 0)
BLUE = ( 0, 0, 255)
BLACK = ( 0, 0, 0)
class Box(pygame.sprite.Sprite):
""" Box the user can jump on """
def __init__(self, width, height):
super().__init__()
self.image = pygame.Surface([width, height])
self.image.fill(BLACK)
self.rect = self.image.get_rect()
class Level():
#create the box_list as a Class attribute that exists for all instances.
box_list = pygame.sprite.Group()
def __init__(self):
# Background image
self.background = None
# Array with width, height, x, and y of platform
level = [[210, 70, 500, 100],
[210, 70, 200, 400],
[210, 70, 600, 300],
]
# Go through the array above and add platforms
if len(Level.box_list) < 3:
for platform in level:
block = Box(platform[0], platform[1])
block.rect.x = platform[2]
block.rect.y = platform[3]
Level.box_list.add(block)
# Update everything on this level
def update(self):
""" Update everything in this level."""
# Level.box_list.update()
def draw(self, DISPLAYSURF):
""" Draw everything on this level. """
# Draw all the sprite lists that we have
Level.box_list.draw(DISPLAYSURF)
When checking for collisions, pygame.sprite.spritecollide will check whether the Sprite's rects are intersecting. You're not updating the player's rect attribute, so its position will be at (0, 0). Make sure they are synchronized:
# update position with calculated velocity
self.x += self.velX
self.y += self.velY
self.rect.x = self.x
self.rect.y = self.y
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
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