Creating a Auto targeting bullet - python

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

Related

Bullet movement not working as expected when player sprite is moving

My game is a top down shooter. When the player is stationary and is shooting in any direction, the bullet goes in the same direction as the mouse. However, when I move diagonally whilst shooting the bullet is no longer in the direction of the mouse position. So basically it works when the player is stationary, but not when I'm moving.
Edit: I will keep trying to fix this but here is a video to better understand the issue https://imgur.com/a/8QRr1PO
Here is the code:
import pygame
from sys import exit
import math
pygame.init()
# window and text
WIDTH = 1280
HEIGHT = 720
FPS = 60
screen = pygame.display.set_mode((WIDTH,HEIGHT))
pygame.display.set_caption('Shooting problem demo')
game_font = pygame.font.Font('freesansbold.ttf', 50)
clock = pygame.time.Clock()
# loads imgs
background = pygame.image.load("background/gamemap.png").convert()
plain_bg = pygame.image.load("background/plain_bg.png").convert()
bullet_img = pygame.image.load("bullets/bluebullet.png").convert_alpha()
class Player(pygame.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.image = pygame.image.load("handgun/move/survivor-move_handgun_0.png").convert_alpha()
self.image = pygame.transform.rotozoom(self.image, 0, 0.35)
self.base_player_image = self.image
self.pos = pos
self.base_player_rect = self.base_player_image.get_rect(center = pos)
self.rect = self.base_player_rect.copy()
self.player_speed = 10 # was 4
self.shoot = False
self.shoot_cooldown = 0
def player_turning(self):
self.mouse_coords = pygame.mouse.get_pos()
self.x_change_mouse_player = (self.mouse_coords[0] - (WIDTH // 2))
self.y_change_mouse_player = (self.mouse_coords[1] - (HEIGHT // 2))
self.angle = int(math.degrees(math.atan2(self.y_change_mouse_player, self.x_change_mouse_player)))
self.angle = (self.angle) % 360
self.image = pygame.transform.rotate(self.base_player_image, -self.angle)
self.rect = self.image.get_rect(center=self.base_player_rect.center)
def player_input(self):
self.velocity_x = 0
self.velocity_y = 0
keys = pygame.key.get_pressed()
if keys[pygame.K_w]:
self.velocity_y = -self.player_speed
if keys[pygame.K_s]:
self.velocity_y = self.player_speed
if keys[pygame.K_d]:
self.velocity_x = self.player_speed
if keys[pygame.K_a]:
self.velocity_x = -self.player_speed
if self.velocity_x != 0 and self.velocity_y != 0: # moving diagonally
self.velocity_x /= math.sqrt(2)
self.velocity_y /= math.sqrt(2)
if keys[pygame.K_SPACE]:
self.shoot = True
self.is_shooting()
else:
self.shoot = False
if event.type == pygame.KEYUP:
if event.key == pygame.K_SPACE:
self.shoot = False
def move(self):
self.base_player_rect.centerx += self.velocity_x
self.base_player_rect.centery += self.velocity_y
self.rect.center = self.base_player_rect.center
def is_shooting(self):
if self.shoot_cooldown == 0 and self.shoot:
self.bullet = Bullet(self.base_player_rect.centerx, self.base_player_rect.centery, self.angle)
self.shoot_cooldown = 20
bullet_group.add(self.bullet)
all_sprites_group.add(self.bullet)
def update(self):
self.player_turning()
self.player_input()
self.move()
if self.shoot_cooldown > 0:
self.shoot_cooldown -= 1
class Bullet(pygame.sprite.Sprite):
def __init__(self, x, y, angle):
super().__init__()
self.image = bullet_img
self.image = pygame.transform.rotozoom(self.image, 0, 0.1)
self.image.set_colorkey((0,0,0))
self.rect = self.image.get_rect()
self.rect.center = (x, y)
self.x = x
self.y = y
self.speed = 10
self.angle = angle
self.x_vel = math.cos(self.angle * (2*math.pi/360)) * self.speed
self.y_vel = math.sin(self.angle * (2*math.pi/360)) * self.speed
self.bullet_lifetime = 750
self.spawn_time = pygame.time.get_ticks()
def bullet_movement(self):
self.x += self.x_vel
self.y += self.y_vel
self.rect.x = int(self.x)
self.rect.y = int(self.y)
if pygame.time.get_ticks() - self.spawn_time > self.bullet_lifetime:
self.kill()
def update(self):
self.bullet_movement()
class Camera(pygame.sprite.Group):
def __init__(self):
super().__init__()
self.offset = pygame.math.Vector2()
self.floor_rect = background.get_rect(topleft = (0,0))
def custom_draw(self):
self.offset.x = player.rect.centerx - (WIDTH // 2)
self.offset.y = player.rect.centery - (HEIGHT // 2)
#draw the floor
floor_offset_pos = self.floor_rect.topleft - self.offset
screen.blit(background, floor_offset_pos)
for sprite in all_sprites_group:
offset_pos = sprite.rect.topleft - self.offset
screen.blit(sprite.image, offset_pos)
# Groups
all_sprites_group = pygame.sprite.Group()
player = Player((900,900))
all_sprites_group.add(player)
bullet_group = pygame.sprite.Group()
camera = Camera()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
screen.blit(plain_bg, (0,0))
camera.custom_draw()
all_sprites_group.update()
pygame.display.update()
clock.tick(FPS)
Actually, there is no problem at all. It is just an optical illusion. The projectile does not move relative to the player, but relative to the camera. The player is always in the center of the screen, because the player doesn't move, but the camera does. When the camera moves, all objects move with the camera.
For example, if you shoot a bullet to the right and move the player up, it will look like the bullet is moving diagonally to the right and down. To the right because it changes position, to the right and down because the player moves upwards.
To illustrate this, I reduced the speed of the player (self.player_speed = 2) and the speed of the bullet (self.speed = 4) and drew the scene on a checkered background:
if event.type == pygame.KEYUP: only makes sens in the event loop, but not in player_input. Shooting only one bullet at once just needs another condition (and not self.shoot):
if keys[pygame.K_SPACE] and not self.shoot:
self.shoot = True
self.is_shooting()
else:
self.shoot = False

pygame - Bullet sprite wrong offset from the ship angle (Vectors2)

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)

Python Pygame Collision [duplicate]

This question already has answers here:
How do I detect collision in pygame?
(5 answers)
How to detect collisions between two rectangular objects or images in pygame
(1 answer)
Closed 2 years ago.
Walls do block me but if i keep walking into them i teleport through. I've tried a number of things and cant seem to figure it out. Things that make it difficult is that my character rotates based on where my mouse position is. self.level_terrain is a sprite.group. update is called from my state manager.
def update(self, clock):
self.dT = clock.get_time()
pressed = pg.key.get_pressed()
mousePos = pg.mouse.get_pos()
self.player.update(mousePos, pressed, self.dT, self.level_terrain)
def get_angle(origin, destination):
"""Returns angle in radians from origin to destination.
This is the angle that you would get if the points were
on a cartesian grid. Arguments of (0,0), (1, -1)
return pi/4 (45 deg) rather than 7/4.
"""
x_dist = destination[0] - origin[0]
y_dist = destination[1] - origin[1]
return atan2(-y_dist, x_dist) % (2 * pi)
class Player(pg.sprite.Sprite):
def __init__(self, center_pos):
super(Player, self).__init__()
self.original_image = pg.image.load("data/resources/images/playerImage.png").convert_alpha()
self.facing_angle = 90
self.start_angle = 90
self.pos = center_pos
self.velocity = [0, 0]
self.set_image()
self.vx = 0
self.vy = 0
self.keys_dict = {
pg.K_w: (0, -1),
pg.K_s: (0, 1),
pg.K_a: (-1, 0),
pg.K_d: (1, 0)}
self.speed = .2
def set_image(self):
angle = self.facing_angle - self.start_angle
self.image = pg.transform.rotate(self.original_image, angle)
self.rect = self.image.get_rect()
def update(self, mouse_pos, pressed, dt, obstacles):
self.facing_angle = degrees(get_angle(self.pos, mouse_pos))
self.set_image()
last = self.rect.copy()
for k in self.keys_dict:
if pressed[k]:
x, y = self.keys_dict[k]
self.vx = x * self.speed * dt
self.vy = y * self.speed * dt
self.pos = (self.pos[0] + (self.vx),
self.pos[1] + (self.vy))
self.rect.center = self.pos
current = self.rect # Just for readability we 'rename' the objects rect attribute to 'current'.
for wall in pg.sprite.spritecollide(self, obstacles, dokill=False):
wall = wall.rect # Just for readability we 'rename' the wall's rect attribute to just 'wall'.
if last.left >= wall.right > current.left: # Collided left side.
current.left = wall.right
elif last.right <= wall.left < current.right: # Collided right side.
current.right = wall.left
elif last.top >= wall.bottom > current.top: # Collided from above.
current.top = wall.bottom
elif last.bottom <= wall.top < current.bottom: # Collided from below.
current.bottom = wall.top
def draw(self, surface):
surface.blit(self.image, self.rect)

Rotating character and sprite wall

I have a sprite that represents my character. This sprite rotates every frame according to my mouse position which in turn makes it so my rectangle gets bigger and smaller depending on where the mouse is.
Basically what I want is to make it so my sprite (Character) doesn't go into the sprite walls. Now since the rect for the walls are larger then the actual pictures seems and my rect keeps growing and shrinking depending on my mouse position it leaves me clueless as for how to make a statement that stops my sprite from moving into the walls in a convincing manner.
I already know for sure that my ColideList is only the blocks that are supposed to be collided with. I found Detecting collision of two sprites that can rotate, but it's in Java and I don't need to check collision between two rotating sprites but one and a wall.
My Character class looks like this:
class Character(pygame.sprite.Sprite):
walking_frame = []
Max_Hp = 100
Current_HP = 100
Alive = True
X_Speed = 0
Y_Speed = 0
Loc_x = 370
Loc_y = 430
size = 15
Current_Weapon = Weapon()
Angle = 0
reloading = False
shot = False
LastFrame = 0
TimeBetweenFrames = 0.05
frame = 0
Walking = False
Blocked = 0
rel_path = "Sprite Images/All.png"
image_file = os.path.join(script_dir, rel_path)
sprite_sheet = SpriteSheet(image_file) #temp
image = sprite_sheet.get_image(0, 0, 48, 48) #Temp
image = pygame.transform.scale(image, (60, 60))
orgimage = image
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.walking_frame.append(self.image)
image = self.sprite_sheet.get_image(48, 0, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(96, 0, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(144, 0, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(0, 48, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(48, 48, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(96, 48, 48, 48)
self.walking_frame.append(image)
image = self.sprite_sheet.get_image(144, 48, 48, 48)
self.walking_frame.append(image)
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = [self.Loc_x,self.Loc_y]
print "Shabat Shalom"
def Shoot(self):
if self.Alive:
if(self.reloading == False):
if(self.Current_Weapon.Clip_Ammo > 0):
bullet = Bullet(My_Man)
bullet_list.add(bullet)
self.Current_Weapon.Clip_Ammo -= 1
def move(self):
if self.Alive:
self.Animation()
self.Loc_x += self.X_Speed
self.Loc_y += self.Y_Speed
Wall_hit_List = pygame.sprite.spritecollide(My_Man, CollideList, False)
self.Blocked = 0
for wall in Wall_hit_List:
if self.rect.right <= wall.rect.left and self.rect.right >= wall.rect.right:
self.Blocked = 1 #right
self.X_Speed= 0
elif self.rect.left <= wall.rect.right and self.rect.left >= wall.rect.left:
self.Blocked = 3 #Left
self.X_Speed = 0
elif self.rect.top <= wall.rect.bottom and self.rect.top >= wall.rect.top:
self.Blocked = 2 #Up
self.Y_Speed = 0
elif self.rect.top >= wall.rect.bottom and self.rect.top <= wall.rect.top:
self.Blocked = 4 #Down
self.Y_Speed = 0
self.image = pygame.transform.rotate(self.orgimage, self.Angle)
self.rect = self.image.get_rect()
self.rect.left, self.rect.top = [self.Loc_x, self.Loc_y]
def Animation(self):
# #Character Walk Animation
if self.X_Speed != 0 or self.Y_Speed != 0:
if(self.Walking == False):
self.LastFrame = time.clock()
self.Walking = True
if (self.frame < len(self.walking_frame)):
self.image = self.walking_frame[self.frame]
self.image = pygame.transform.scale(self.image, (60, 60))
self.orgimage = self.image
self.frame += 1
else:
self.frame = 0
else:
if self.frame != 0:
self.frame = 0
self.image = self.walking_frame[self.frame]
self.image = pygame.transform.scale(self.image, (60, 60))
self.orgimage = self.image
if self.Walking and time.clock() - self.LastFrame > self.TimeBetweenFrames:
self.Walking = False
def CalAngle(self,X,Y):
angle = math.atan2(self.Loc_x - X, self.Loc_y - Y)
self.Angle = math.degrees(angle) + 180
My Wall class looks like this:
class Wall(pygame.sprite.Sprite):
def __init__(self, PosX, PosY, image_file, ImageX,ImageY):
pygame.sprite.Sprite.__init__(self)
self.sprite_sheet = SpriteSheet(image_file)
self.image = self.sprite_sheet.get_image(ImageX, ImageY, 64, 64)
self.image = pygame.transform.scale(self.image, (32, 32))
self.image.set_colorkey(Black)
self.rect = self.image.get_rect()
self.rect.x = PosX
self.rect.y = PosY
My BuildWall function looks like this:
def BuildWall(NumberOfBlocks,TypeBlock,Direction,X,Y,Collide):
for i in range(NumberOfBlocks):
if Direction == 1:
wall = Wall(X + (i * 32), Y, spriteList, 0, TypeBlock)
wall_list.add(wall)
if Direction == 2:
wall = Wall(X - (i * 32), Y, spriteList, 0, TypeBlock)
wall_list.add(wall)
if Direction == 3:
wall = Wall(X, Y + (i * 32), spriteList, 0, TypeBlock)
wall_list.add(wall)
if Direction == 4:
wall = Wall(X, Y - (i * 32), spriteList, 0, TypeBlock)
wall_list.add(wall)
if(Collide):
CollideList.add(wall)
Lastly my walking events looks like this:
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: #Press escape also leaves game
Game = False
elif event.key == pygame.K_w and My_Man.Blocked != 2:
My_Man.Y_Speed = -3
elif event.key == pygame.K_s and My_Man.Blocked != 4:
My_Man.Y_Speed = 3
elif event.key == pygame.K_a and My_Man.Blocked != 3:
My_Man.X_Speed = -3
elif event.key == pygame.K_d and My_Man.Blocked != 1:
My_Man.X_Speed = 3
elif event.key == pygame.K_r and (My_Man.reloading == False):
lastReloadTime = time.clock()
My_Man.reloading = True
if (My_Man.Current_Weapon.Name == "Pistol"):
My_Man.Current_Weapon.Clip_Ammo = My_Man.Current_Weapon.Max_Clip_Ammo
else:
My_Man.Current_Weapon.Clip_Ammo, My_Man.Current_Weapon.Max_Ammo = Reload(My_Man.Current_Weapon.Max_Ammo,My_Man.Current_Weapon.Clip_Ammo,My_Man.Current_Weapon.Max_Clip_Ammo)
elif event.type == pygame.KEYUP:
if event.key == pygame.K_w:
My_Man.Y_Speed = 0
elif event.key == pygame.K_s:
My_Man.Y_Speed = 0
elif event.key == pygame.K_a:
My_Man.X_Speed = 0
elif event.key == pygame.K_d:
My_Man.X_Speed = 0
It all depends on how your sprite looks and how you want the result to be. There are 3 different types of collision detection I believe could work in your scenario.
Keeping your rect from resizing
Since the image is getting larger when you rotate it, you could compensate by just removing the extra padding and keep the image in it's original size.
Say that the size of the original image is 32 pixels wide and 32 pixels high. After rotating, the image is 36 pixels wide and 36 pixels high. We want to take out the center of the image (since the padding is added around it).
To take out the center of the new image we simply take out a subsurface of the image the size of our previous rectangle centered inside the image.
def rotate(self, degrees):
self.rotation = (self.rotation + degrees) % 360 # Keep track of the current rotation.
self.image = pygame.transform.rotate(self.original_image, self.rotation))
center_x = self.image.get_width() // 2
center_y = self.image.get_height() // 2
rect_surface = self.rect.copy() # Create a new rectangle.
rect_surface.center = (center_x, center_y) # Move the new rectangle to the center of the new image.
self.image = self.image.subsurface(rect_surface) # Take out the center of the new image.
Since the size of the rectangle doesn't change we don't need to do anything to recalculate it (in other words: self.rect = self.image.get_rect() will not be necessary).
Rectangular detection
From here you just use pygame.sprite.spritecollide (or if you have an own function) as usual.
def collision_rect(self, walls):
last = self.rect.copy() # Keep track on where you are.
self.rect.move_ip(*self.velocity) # Move based on the objects velocity.
current = self.rect # Just for readability we 'rename' the objects rect attribute to 'current'.
for wall in pygame.sprite.spritecollide(self, walls, dokill=False):
wall = wall.rect # Just for readability we 'rename' the wall's rect attribute to just 'wall'.
if last.left >= wall.right > current.left: # Collided left side.
current.left = wall.right
elif last.right <= wall.left < current.right: # Collided right side.
current.right = wall.left
elif last.top >= wall.bottom > current.top: # Collided from above.
current.top = wall.bottom
elif last.bottom <= wall.top < current.bottom: # Collided from below.
current.bottom = wall.top
Circular collision
This probably will not work the best if you're tiling your walls, because you'll be able to go between tiles depending on the size of the walls and your character. It is good for many other things so I'll keep this in.
If you add the attribute radius to your player and wall you can use pygame.sprite.spritecollide and pass the callback function pygame.sprite.collide_circle. You don't need a radius attribute, it's optional. But if you don't pygame will calculate the radius based on the sprites rect attribute, which is unnecessary unless the radius is constantly changing.
def collision_circular(self, walls):
self.rect.move_ip(*self.velocity)
current = self.rect
for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_circle):
distance = self.radius + wall.radius
dx = current.centerx - wall.rect.centerx
dy = current.centery - wall.rect.centery
multiplier = ((distance ** 2) / (dx ** 2 + dy ** 2)) ** (1/2)
current.centerx = wall.rect.centerx + (dx * multiplier)
current.centery = wall.rect.centery + (dy * multiplier)
Pixel perfect collision
This is the hardest to implement and is performance heavy, but can give you the best result. We'll still use pygame.sprite.spritecollide, but this time we're going to pass pygame.sprite.collide_mask as the callback function. This method require that your sprites have a rect attribute and a per pixel alpha Surface or a Surface with a colorkey.
A mask attribute is optional, if there is none the function will create one temporarily. If you use a mask attribute you'll need to change update it every time your sprite image is changed.
The hard part of this kind of collision is not to detect it but to respond correctly and make it move/stop appropriately. I made a buggy example demonstrating one way to handle it somewhat decently.
def collision_mask(self, walls):
last = self.rect.copy()
self.rect.move_ip(*self.velocity)
current = self.rect
for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_mask):
if not self.rect.center == last.center:
self.rect.center = last.center
break
wall = wall.rect
x_distance = current.centerx - wall.centerx
y_distance = current.centery - wall.centery
if abs(x_distance) > abs(y_distance):
current.centerx += (x_distance/abs(x_distance)) * (self.velocity[0] + 1)
else:
current.centery += (y_distance/abs(y_distance)) * (self.velocity[1] + 1)
Full code
You can try out the different examples by pressing 1 for rectangular collision, 2 for circular collision and 3 for pixel-perfect collision. It's a little buggy in some places, the movement isn't top notch and isn't ideal performance wise, but it's just a simple demonstration.
import pygame
pygame.init()
SIZE = WIDTH, HEIGHT = (256, 256)
clock = pygame.time.Clock()
screen = pygame.display.set_mode(SIZE)
mode = 1
modes = ["Rectangular collision", "Circular collision", "Pixel perfect collision"]
class Player(pygame.sprite.Sprite):
def __init__(self, pos):
super(Player, self).__init__()
self.original_image = pygame.Surface((32, 32))
self.original_image.set_colorkey((0, 0, 0))
self.image = self.original_image.copy()
pygame.draw.ellipse(self.original_image, (255, 0, 0), pygame.Rect((0, 8), (32, 16)))
self.rect = self.image.get_rect(center=pos)
self.rotation = 0
self.velocity = [0, 0]
self.radius = self.rect.width // 2
self.mask = pygame.mask.from_surface(self.image)
def rotate_clipped(self, degrees):
self.rotation = (self.rotation + degrees) % 360 # Keep track of the current rotation
self.image = pygame.transform.rotate(self.original_image, self.rotation)
center_x = self.image.get_width() // 2
center_y = self.image.get_height() // 2
rect_surface = self.rect.copy() # Create a new rectangle.
rect_surface.center = (center_x, center_y) # Move the new rectangle to the center of the new image.
self.image = self.image.subsurface(rect_surface) # Take out the center of the new image.
self.mask = pygame.mask.from_surface(self.image)
def collision_rect(self, walls):
last = self.rect.copy() # Keep track on where you are.
self.rect.move_ip(*self.velocity) # Move based on the objects velocity.
current = self.rect # Just for readability we 'rename' the objects rect attribute to 'current'.
for wall in pygame.sprite.spritecollide(self, walls, dokill=False):
wall = wall.rect # Just for readability we 'rename' the wall's rect attribute to just 'wall'.
if last.left >= wall.right > current.left: # Collided left side.
current.left = wall.right
elif last.right <= wall.left < current.right: # Collided right side.
current.right = wall.left
elif last.top >= wall.bottom > current.top: # Collided from above.
current.top = wall.bottom
elif last.bottom <= wall.top < current.bottom: # Collided from below.
current.bottom = wall.top
def collision_circular(self, walls):
self.rect.move_ip(*self.velocity)
current = self.rect
for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_circle):
distance = self.radius + wall.radius
dx = current.centerx - wall.rect.centerx
dy = current.centery - wall.rect.centery
multiplier = ((distance ** 2) / (dx ** 2 + dy ** 2)) ** (1/2)
current.centerx = wall.rect.centerx + (dx * multiplier)
current.centery = wall.rect.centery + (dy * multiplier)
def collision_mask(self, walls):
last = self.rect.copy()
self.rect.move_ip(*self.velocity)
current = self.rect
for wall in pygame.sprite.spritecollide(self, walls, dokill=False, collided=pygame.sprite.collide_mask):
if not self.rect.center == last.center:
self.rect.center = last.center
break
wall = wall.rect
x_distance = current.centerx - wall.centerx
y_distance = current.centery - wall.centery
if abs(x_distance) > abs(y_distance):
current.centerx += (x_distance/abs(x_distance)) * (self.velocity[0] + 1)
else:
current.centery += (y_distance/abs(y_distance)) * (self.velocity[1] + 1)
def update(self, walls):
self.rotate_clipped(1)
if mode == 1:
self.collision_rect(walls)
elif mode == 2:
self.collision_circular(walls)
else:
self.collision_mask(walls)
class Wall(pygame.sprite.Sprite):
def __init__(self, pos):
super(Wall, self).__init__()
size = (32, 32)
self.image = pygame.Surface(size)
self.image.fill((0, 0, 255)) # Make the Surface blue.
self.image.set_colorkey((0, 0, 0)) # Will not affect the image but is needed for collision with mask.
self.rect = pygame.Rect(pos, size)
self.radius = self.rect.width // 2
self.mask = pygame.mask.from_surface(self.image)
def show_rects(player, walls):
for wall in walls:
pygame.draw.rect(screen, (1, 1, 1), wall.rect, 1)
pygame.draw.rect(screen, (1, 1, 1), player.rect, 1)
def show_circles(player, walls):
for wall in walls:
pygame.draw.circle(screen, (1, 1, 1), wall.rect.center, wall.radius, 1)
pygame.draw.circle(screen, (1, 1, 1), player.rect.center, player.radius, 1)
def show_mask(player, walls):
for wall in walls:
pygame.draw.rect(screen, (1, 1, 1), wall.rect, 1)
for pixel in player.mask.outline():
pixel_x = player.rect.x + pixel[0]
pixel_y = player.rect.y + pixel[1]
screen.set_at((pixel_x, pixel_y), (1, 1, 1))
# Create walls around the border.
walls = pygame.sprite.Group()
walls.add(Wall(pos=(col, 0)) for col in range(0, WIDTH, 32))
walls.add(Wall(pos=(0, row)) for row in range(0, HEIGHT, 32))
walls.add(Wall(pos=(col, HEIGHT - 32)) for col in range(0, WIDTH, 32))
walls.add(Wall(pos=(WIDTH - 32, row)) for row in range(0, HEIGHT, 32))
walls.add(Wall(pos=(WIDTH//2, HEIGHT//2))) # Obstacle in the middle of the screen
player = Player(pos=(64, 64))
speed = 2 # Speed of the player.
while True:
screen.fill((255, 255, 255))
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_a:
player.velocity[0] = -speed
elif event.key == pygame.K_d:
player.velocity[0] = speed
elif event.key == pygame.K_w:
player.velocity[1] = -speed
elif event.key == pygame.K_s:
player.velocity[1] = speed
elif pygame.K_1 <= event.key <= pygame.K_3:
mode = event.key - 48
print(modes[mode - 1])
elif event.type == pygame.KEYUP:
if event.key == pygame.K_a or event.key == pygame.K_d:
player.velocity[0] = 0
elif event.key == pygame.K_w or event.key == pygame.K_s:
player.velocity[1] = 0
player.update(walls)
walls.draw(screen)
screen.blit(player.image, player.rect)
if mode == 1:
show_rects(player, walls) # Show rectangles for circular collision detection.
elif mode == 2:
show_circles(player, walls) # Show circles for circular collision detection.
else:
show_mask(player, walls) # Show mask for pixel perfect collision detection.
pygame.display.update()
Last note
Before programming any further you really need to refactor your code. I tried to read some of your code but it's really hard to understand. Try follow Python's naming conventions, it'll make it much easier for other programmers to read and understand your code, which makes it easier for them to help you with your questions.
Just following these simple guidelines will make your code much readable:
Variable names should contain only lowercase letters. Names with more than 1 word should be separated with an underscore. Example: variable, variable_with_words.
Functions and attributes should follow the same naming convention as variables.
Class names should start with an uppercase for every word and the rest should be lowercase. Example: Class, MyClass. Known as CamelCase.
Separate methods in classes with one line, and functions and classes with two lines.
I don't know what kind of IDE you use, but Pycharm Community Edition is a great IDE for Python. It'll show you when you're breaking Python conventions (and much more of course).
It's important to note that these are conventions and not rules. They are meant to make code more readable and not to be followed strictly. Break them if you think it improves readability.

how to make a sprite bounce of the edges of the window in pygame

So far I have my program bouncing from left to right, but now I want to have in bounce of the walls. How can I get to that?
delta = 5
u = 1 # x coordinate
u += delta
if u >= 440: #480 is the window size, I have it stopping at 440
delta = -5 #Go back by 5
elif u < 0:
delta = 5 #Go up by 5
Bigger Snip it of code, It has the solution but the sprite goes off the screen, To Run the code make sure you have a picture called "bird.png" or change the picture:
import pygame
import random
class mySprite(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("bird.png")
self.rect = self.image.get_rect()
## slef.rect(50,50,self.image.get_width(), self.image.get_height())
def update(self,x,y):
self.rect.x = x
self.rect.y = y
class newSprite(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("ball.png")
self.rect = self.image.get_rect()
def update(self,x,y):
self.rect.x = x
self.rect.y = y
class otherSprite(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("bird.png")
self.rect = self.image.get_rect()
def update(self,x,y):
self.rect.x = x
self.rect.y = y
def main():
""" Set up the game and run the main game loop """
pygame.init() # prepare the pygame module for use
surfaceSz = 480 # Desired physical surface size, in pixels.
# Create surface of (width, height), and its window.
main_surface = pygame.display.set_mode((surfaceSz, surfaceSz))
small_rect = (300, 200, 150, 90)
some_color = (255,0,0)
main_surface.fill((0,200,255))
s = mySprite()
all_sprites = pygame.sprite.Group()
s.add(all_sprites)
delta_x = 5
delta_y = 5
u = 1 # x coord
v = 1 # y coord
clock = pygame.time.Clock()
while True:
ev = pygame.event.poll() # look for any event
if ev.type == pygame.QUIT: # window close button clicked?
break # ... leave game loop
main_surface.fill((0,200,255))
#u += delta
#if u >= 440:
# delta = -5
#elif u < 0:
# delta = 5
u += delta_x
v += delta_y
if (v >=440): #assuming your y axis is 440 pixels
delta_y = -5
elif v<=0 :
delta_y = 5
if u >= 440: #440 pixels u have
delta = -5
elif u < 0:
delta = 5 #Go up by 5
all_sprites.update(u,50)
pos =(random.randint(0, surfaceSz), random.randint(0, surfaceSz))
all_sprites.draw(main_surface)
# Now the surface is ready, tell pygame to display it!
pygame.display.flip()
clock.tick(60)
pygame.quit() # once we leave the loop, close the window.
main()
here's a moving bird/ball that is bouncing off the walls,
first of all you need only one type of class for sprites, simply create different instances with different images. Hope it helps
import sys, pygame, random
SIZE = width, height = 640, 480
SPEED = [2, 2]
black = 0, 0, 0
class mySprite(pygame.sprite.Sprite):
def __init__(self, image="ball.bmp", speed=[2,2]):
pygame.sprite.Sprite.__init__(self)
self.speed = speed
self.image = pygame.image.load(image)
self.rect = self.image.get_rect()
def update(self):
global SIZE
#i used 110 px because it's the size of the image, but it's better to use the rect property of the pygame
if (self.rect.x <0) or (self.rect.x > 640-110):
self.speed[0] *= -1
if (self.rect.y<0) or (self.rect.y > 480-110):
self.speed[1] *= -1
self.rect.x =self.rect.x + self.speed[0]
self.rect.y =self.rect.y + self.speed[1]
#OR: self.rect = self.rect.move(self.speed)
def draw(self, screen):
screen.blit(self.image, self.rect)
#
pygame.init()
screen = pygame.display.set_mode(SIZE)
iaka = mySprite()
while 1:
for event in pygame.event.get():
if event.type == pygame.QUIT: sys.exit()
screen.fill(black)
iaka.draw(screen)
iaka.update()
pygame.display.flip()
make a y coordinate as well
delta_y = 5
delta_x = 5
u = 1 # the x coordinate
v = 1 # the y coordinate
u +=delta_x
v +=delta_y
if (v >=300): #assuming your y axis is 300 pixels
delta_y = -5
elif v<=0 :
delta_y = 5
if u >= 440: #440 pixels u have
delta_x = -5
elif u < 0:
delta_x = 5 #Go up by 5

Categories

Resources