I've found in the pygame docs that the .rect attribute of a sprite is often initialised with the same width and the same height as the .image attribute.
However, you can change the .rect width or height so that the .rect becomes larger or smaller than the .image but the origins (topleft corners) of both .image and .rect are always the same point.
So here is my question:
Is there a way to create an offset between the origins (ie topleft corners) of the .rect and the .image of a sprite?
I know you can avoid this by:
creating and using a second Rect attribute (for instance a .collide_rect) that would be updated each time the .rect would
using transparency in the provided .image
But those methods are quite inefficient and I would really appreciate a "Yes/No" answer.
I've searched in the official docs for hours but I didn't find the way to create such an offset so I suppose the answer to my question is "No" (even the .inflate_ip() method doesn't work since it doesn't keep the center of the .rect in place as written in the docs).
You could give the sprite class an additional offset attribute (a Vector2) and then iterate over the sprites with a for loop and add this offset to the .rect.topleft position when you're blitting the images.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((220, 120))
self.image.fill(pg.Color('steelblue2'))
self.rect = self.image.get_rect(center=pos)
self.rect.inflate_ip(-42, -72) # Make the rect smaller.
self.vel = Vector2(0, 0)
self.pos = Vector2(pos)
# Offset is half of the inflation size,
# so that the rect will be centered.
self.offset = Vector2(-21, -36)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((300, 200), all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_d:
player.vel.x = 5
elif event.type == pg.KEYUP:
if event.key == pg.K_d:
player.vel.x = 0
all_sprites.update()
screen.fill((30, 30, 30))
# Assign `blit` to a local variable to improve the performance.
screen_blit = screen.blit
for sprite in all_sprites:
# Now blit the sprites at topleft + offset.
screen_blit(sprite.image, sprite.rect.topleft+sprite.offset)
pg.draw.rect(screen, (250, 30, 0), sprite.rect, 2)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
For the sake of completeness, here's the solution with a second, scaled "hitbox" rect and a custom collided callback function that you can pass to one of the collision detection functions like pygame.sprite.spritecollide.
import pygame as pg
from pygame.math import Vector2
class Player(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = pg.Surface((70, 40))
self.image.fill(pg.Color('steelblue4'))
self.rect = self.image.get_rect(center=pos)
# A inflated rect as the hitbox.
self.hitbox = self.rect.copy()
self.hitbox.inflate_ip(-42, -22)
self.vel = Vector2(0, 0)
self.pos = Vector2(pos)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
self.hitbox.center = self.pos # Also update the hitbox coords.
def collided(sprite, other):
"""Check if the hitboxes of the two sprites collide."""
return sprite.hitbox.colliderect(other.hitbox)
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Player((300, 200), all_sprites)
enemies = pg.sprite.Group(
Player((100, 250), all_sprites),
Player((400, 300), all_sprites),
)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEMOTION:
player.pos = event.pos
all_sprites.update()
# Pass the custom collided callback function to spritecollide.
collided_sprites = pg.sprite.spritecollide(
player, enemies, False, collided)
for sp in collided_sprites:
print('Collision', sp)
screen.fill((30, 30, 30))
all_sprites.draw(screen)
for sprite in all_sprites:
# Draw rects and hitboxes.
pg.draw.rect(screen, (0, 230, 0), sprite.rect, 2)
pg.draw.rect(screen, (250, 30, 0), sprite.hitbox, 2)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
Related
I'm new to Pygame and trying to learn about sprites and collision detection. I found some code and tried to add some functionality to it.
The basic idea is that I have a polygon which can be rotated in place. When the user presses SPACEBAR, the polygon fires a projectile which can bounce around the frame. If the projectile hits the polygon again, I would like to quite the program
First of all, here is the code
import pygame
class Player(pygame.sprite.Sprite):
def __init__(self):
super().__init__()
self.image = pygame.Surface((32, 32))
self.image.fill((0, 0, 0))
self.image.set_colorkey((0, 0, 0))
self.fire_from = (36, 20)
pygame.draw.polygon(self.image, pygame.Color('dodgerblue'), ((0, 0), (32, 16), (0, 32)))
self.org_image = self.image.copy()
self.angle = 0
self.direction = pygame.Vector2(1, 0)
self.rect = self.image.get_rect(center=(200, 200))
self.pos = pygame.Vector2(self.rect.center)
def update(self, events, dt):
for e in events:
if e.type == pygame.KEYDOWN:
if e.key == pygame.K_SPACE:
bullet = Projectile(self.fire_from, self.direction.normalize())
self.groups()[0].add(bullet)
pressed = pygame.key.get_pressed()
if pressed[pygame.K_LEFT]:
self.angle += 3
if pressed[pygame.K_RIGHT]:
self.angle -= 3
self.direction = pygame.Vector2(1, 0).rotate(-self.angle)
self.image = pygame.transform.rotate(self.org_image, self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
class Projectile(pygame.sprite.Sprite):
def __init__(self, pos, direction):
super().__init__()
self.image = pygame.Surface((8, 8))
self.image.fill((0, 0, 0))
self.image.set_colorkey((0, 0, 0))
pygame.draw.circle(self.image, pygame.Color('orange'), (4, 4), 4)
self.rect = self.image.get_rect(center=pos)
self.direction = direction
self.pos = pygame.Vector2(self.rect.center)
self.max_bounce = 10
def update(self, events, dt):
#print(self.pos)
# Bounding box of the screen
screen_r = pygame.display.get_surface().get_rect()
# where we would move next
next_pos = self.pos + self.direction * dt
# we hit a wall
if not screen_r.contains(self.rect):
# after 10 hits, destroy self
self.max_bounce -= 1
if self.max_bounce == 0:
return self.kill()
# horizontal reflection
if next_pos.x > screen_r.right or next_pos.x < screen_r.left:
self.direction.x *= -1
# vertical reflection
if next_pos.y > screen_r.bottom or next_pos.y < screen_r.top:
self.direction.y *= -1
# move after applying reflection
next_pos = self.pos + self.direction * dt
# set the new position
self.pos = next_pos
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
tank = Player()
sprites = pygame.sprite.Group()
sprites.add(tank)
#print(tank.groups()[0])
clock = pygame.time.Clock()
dt = 0
running = True
while running:
#print(tank.groups()[0])
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
sprites.update(events, dt)
screen.fill((30, 30, 30))
sprites.draw(screen)
pygame.display.update()
## ALWAYS DETECTS A COLLISION, WHY?
if len(tank.groups()[0])>1:
if pygame.sprite.spritecollideany(tank, tank.groups()[0]):
running = False
dt = clock.tick(60)
if __name__ == '__main__':
main()
I gather here that the Player class (i.e. my tank) itself has a sprite groups, to which it adds each projectile as its fired. Furthermore, the projectile originates from within the rectangle for the polygon (hence it always initially collides)
I would like to only add the projectile to the sprite group if it has made at least 1 bounce, is that possible? Or... is there a better way to do this?
Any help or pointers please?
Thanks
Null
The tank collides with itself, because it is a member of tank.groups()[0]:
if pygame.sprite.spritecollideany(tank, tank.groups()[0]):
Add a Group that contains just the bullets:
class Player(pygame.sprite.Sprite):
def __init__(self):
# [...]
self.bullets = pygame.sprite.Group()
def update(self, events, dt):
for e in events:
if e.type == pygame.KEYDOWN:
if e.key == pygame.K_SPACE:
bullet = Projectile(self.fire_from, self.direction.normalize())
self.groups()[0].add(bullet)
self.bullets.add(bullet)
# [...]
Use this Group for the collision test:
def main():
# [...]
while running:
# [...]
pygame.display.update()
if pygame.sprite.spritecollideany(tank, tank.bullets):
running = False
dt = clock.tick(60)
I am making a game with Pygame. For this game I need to be able to detect not only that two rectangles have collided, but also the point of collision between them. I looked at the documentation but couldn't seem to find any answers.
Is something like this possible?
You can use Rect.clip:
crops a rectangle inside another
clip(Rect) -> Rect
Returns a new rectangle that is cropped to be completely inside the argument Rect. If the two rectangles do not overlap to begin with, a Rect with 0 size is returned.
Here's an example:
import pygame
import random
class Stuff(pygame.sprite.Sprite):
def __init__(self, pos, color, *args):
super().__init__(*args)
self.image = pygame.Surface((30, 30))
self.image.fill(color)
self.rect = self.image.get_rect(center=pos)
self.pos = pygame.Vector2(pos)
def update(self):
self.rect.center = self.pos
def main():
pygame.init()
screen = pygame.display.set_mode((500, 500))
screen_rect = screen.get_rect()
font = pygame.font.SysFont(None, 26)
clock = pygame.time.Clock()
sprites = pygame.sprite.Group()
blocks = pygame.sprite.Group()
movement = {
pygame.K_UP: ( 0, -1),
pygame.K_DOWN: ( 0, 1),
pygame.K_LEFT: (-1, 0),
pygame.K_RIGHT: ( 1, 0)
}
for _ in range(15):
x, y = random.randint(0, 500), random.randint(0, 500)
color = random.choice(['green', 'yellow'])
Stuff((x, y), pygame.Color(color), sprites, blocks)
player = Stuff(screen_rect.center, pygame.Color('dodgerblue'))
sprites.add(player)
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
pressed = pygame.key.get_pressed()
move = pygame.Vector2()
for dir in (movement[key] for key in movement if pressed[key]):
move += dir
if move.length() > 0: move.normalize_ip()
player.pos += move * dt/5
sprites.update()
screen.fill(pygame.Color('black'))
sprites.draw(screen)
for block in pygame.sprite.spritecollide(player, blocks, False):
clip = player.rect.clip(block.rect)
pygame.draw.rect(screen, pygame.Color('red'), clip)
hits = [edge for edge in ['bottom', 'top', 'left', 'right'] if getattr(clip, edge) == getattr(player.rect, edge)]
text = font.render(f'Collision at {", ".join(hits)}', True, pygame.Color('white'))
screen.blit(text, (20, 20))
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()
I'm quite new to pygame and came across a bug that i just can't fix on my own. I'm trying to program a Flappy Bird game. The Problem is that the collision detection works, but it also messes with my sprites. If i manage to get past the first obstacle while playing, then the gap resets itself randomly. But the gap should always be the same, just on another position. If i remove the collision detection, it works perfectly fine. Any ideas?
import pygame
import random
randomy = random.randint(-150, 150)
class Bird(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((25,25))
self.image.fill((255,255,255))
self.rect = self.image.get_rect()
self.rect.center = (100, 200)
self.velocity = 0.05
self.acceleration =0.4
def update(self):
self.rect.y += self.velocity
self.velocity += self.acceleration
if self.rect.bottom > 590:
self.velocity = 0
self.acceleration = 0
class Pipe1(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((85, 500))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
self.rect.center = (500, randomy)
self.randomyupdate = random.randint(-150, 150)
def update(self):
self.rect.x -= 2
if self.rect.x < -90:
self.randomyupdate = random.randint(-150, 150)
self.rect.center = (450, self.randomyupdate)
class Pipe2(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((85, 500))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
self.rect.center = (500, (randomy +640))
def update(self):
self.rect.x -= 2
self.randomyupdate = Pipe1.randomyupdate
if self.rect.x < -90:
self.rect.center = (450, (self.randomyupdate + 640))
pygame.init()
pygame.mouse.set_visible(1)
pygame.key.set_repeat(1, 30)
pygame.display.set_caption('Crappy Bird')
clock = pygame.time.Clock()
Bird_sprite = pygame.sprite.Group()
Pipe_sprite = pygame.sprite.Group()
Bird = Bird()
Pipe1 = Pipe1()
Pipe2 = Pipe2 ()
Bird_sprite.add(Bird)
Pipe_sprite.add(Pipe1)
Pipe_sprite.add(Pipe2)
def main():
running = True
while running:
clock.tick(60)
screen = pygame.display.set_mode((400,600))
screen.fill((0,0,0))
Bird_sprite.update()
Pipe_sprite.update()
Bird_sprite.draw(screen)
Pipe_sprite.draw(screen)
The line im talking about:
collide = pygame.sprite.spritecollideany(Bird, Pipe_sprite)
if collide:
running = False
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.event.post(pygame.event.Event(pygame.QUIT))
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
Bird.rect.y -= 85
Bird.velocity = 0.05
Bird.acceleration = 0.4
Bird.rect.y += Bird.velocity
Bird.velocity += Bird.acceleration
pygame.display.flip()
if __name__ == '__main__':
main()
This has nothing to do with the collision detection, it has to do with the order of the sprites in the sprite group which can vary because sprite groups use dictionaries internally which are unordered (in Python versions < 3.6). So if the Pipe1 sprite comes first in the group, the game will work correctly, but if the Pipe2 sprite comes first, then its update method is also called first and the previous randomyupdate of Pipe1 is used to set the new centery coordinate of the sprite.
To fix this you could either turn the sprite group into an ordered group, e.g.
Pipe_sprite = pygame.sprite.OrderedUpdates()
or update the rect of Pipe2 each frame,
def update(self):
self.rect.x -= 2
self.rect.centery = Pipe1.randomyupdate + 640
if self.rect.x < -90:
self.rect.center = (450, Pipe1.randomyupdate + 640)
Also, remove the global randomy variable and always use the randomyupdate attribute of Pipe1.
I am making a flappy bird clone game, using pygame. I want to draw pillars by using Sprite.draw. I made a Pillar class and initialized it with two rectangles p_upper and p_lower on the left side of the screen, coming towards the right side with the help of the update function of the sprite. But the screen is only showing the p_lower pillar. Can anyone help?
class Pillar(pygame.sprite.Sprite):
# the "h" parameter is height of upper pillar upto gap
# "w" is the width of the pillar
# pillar is coming from left to right
def __init__(self, w, h, gap):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((w, h))
self.image.fill(green)
self.p_upper = self.rect = self.image.get_rect()
self.p_upper.topleft = (-w, 0)
self.image = pygame.Surface((w, HEIGHT - (h + gap)))
self.image.fill(green)
self.p_lower = self.rect = self.image.get_rect()
self.p_lower.topleft = (-w, h + gap)
def update(self):
self.p_upper.x += 1
self.p_lower.x += 1
Because of the following two lines:
self.p_upper = self.rect = self.image.get_rect()
and...
self.p_lower = self.rect = self.image.get_rect()
These are both grabbing the same reference to the self.rect. The first line runs and assigns the rect reference to p_upper. Then the same reference is assigned to p_lower. Because it's the same reference, when you update the location of the lower rectangle, you're actually updating both.
Using a sprite that consists of two rects and images isn't a good solution for this problem. I suggest to create two separate sprites with their own image and rect. To create two sprite instances at the same time and to add them to a sprite group, you can write a short function as you can see in this example:
import pygame as pg
from pygame.math import Vector2
green = pg.Color('green')
HEIGHT = 480
class Pillar(pg.sprite.Sprite):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w, h))
self.image.fill(green)
self.rect = self.image.get_rect(topleft=(x, y))
def update(self):
self.rect.x += 1
def create_pillars(w, h, gap, sprite_group):
sprite_group.add(Pillar(0, 0, w, h-gap))
sprite_group.add(Pillar(0, HEIGHT-(h+gap), w, h+gap))
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
create_pillars(50, 170, 0, all_sprites)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.KEYDOWN:
if event.key == pg.K_a:
create_pillars(50, 170, 15, all_sprites)
elif event.key == pg.K_s:
create_pillars(50, 170, 30, all_sprites)
elif event.key == pg.K_d:
create_pillars(50, 100, -60, all_sprites)
all_sprites.update()
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
I am attempting to create a bullet for my 2d shooter game.
I have created the bullet and it is displayed on the screen when the user presses the left mouse button, however I must now make it move.
Each bullet is added to the bullet group.
What would be the easiest way to update the position of the bullet?
This is the while loop. GameClass.Game.getevent is called, which states that if lmb is pressed, then the code below it will be run.
while play:
display.fill(WHITE)
GameClass.Game.getevent(player1)
platforms.draw(display)
sprites.draw(display)
bullets.draw(display)
pygame.display.update()
The following is part of a different file. The shoot function is part of the 'player' class, which has been cut off due to irrelevance.
def shoot(self):
bullet=Bullet(5,self.bullets,self.x,self.y)
self.bullets.add(bullet)
class Bullet(pygame.sprite.Sprite):
def __init__(self,speed,bullets,x,y):
super().__init__()
self.speed=speed
self.image=pygame.Surface((10,10))
self.rect=self.image.get_rect()
self.bullets=bullets
self.x=x
self.y=y
Bullet.movebullet(self)
def movebullet(self):
self.rect.center=((self.x+self.speed),self.y)
What would be the best way of updating the position of each individual bullet in the group?
Here's a complete solution. The player has a direction attribute which I change to 'left' or 'right' when the player presses one of the corresponding keys. When a bullet is created, I pass the player.direction to the bullet instance and then set its image and velocity depending on the direction.
In order to move the sprites, I add the velocity to the position (which are lists in this example) and finally I update the rect because it's needed for the blitting and collision detection. Actually, I'd use pygame.math.Vector2s for the pos and velocity, but I'm not sure if you are familiar with vectors.
import pygame as pg
pg.init()
screen = pg.display.set_mode((640, 480))
screen_rect = screen.get_rect()
PLAYER_IMG = pg.Surface((30, 50))
PLAYER_IMG.fill(pg.Color('dodgerblue1'))
BULLET_IMG = pg.Surface((16, 8))
pg.draw.polygon(BULLET_IMG, pg.Color('sienna1'), [(0, 0), (16, 4), (0, 8)])
BULLET_IMG_LEFT = pg.transform.flip(BULLET_IMG, True, False)
class Player(pg.sprite.Sprite):
def __init__(self, pos):
super().__init__()
self.image = PLAYER_IMG
self.rect = self.image.get_rect(center=pos)
self.velocity = [0, 0]
self.pos = [pos[0], pos[1]]
self.direction = 'right'
def update(self):
self.pos[0] += self.velocity[0]
self.pos[1] += self.velocity[1]
self.rect.center = self.pos
class Bullet(pg.sprite.Sprite):
def __init__(self, pos, direction):
super().__init__()
if direction == 'right':
self.image = BULLET_IMG
self.velocity = [9, 0]
else:
self.image = BULLET_IMG_LEFT
self.velocity = [-9, 0]
self.rect = self.image.get_rect(center=pos)
self.pos = [pos[0], pos[1]]
def update(self):
# Add the velocity to the pos to move the sprite.
self.pos[0] += self.velocity[0]
self.pos[1] += self.velocity[1]
self.rect.center = self.pos # Update the rect as well.
# Remove bullets if they're outside of the screen area.
if not screen_rect.contains(self.rect):
self.kill()
def main():
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
bullets = pg.sprite.Group()
player = Player((100, 300))
all_sprites.add(player)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEBUTTONDOWN:
if event.button == 1:
bullet = Bullet(player.rect.center, player.direction)
bullets.add(bullet)
all_sprites.add(bullet)
elif event.type == pg.KEYDOWN:
if event.key == pg.K_d:
player.velocity[0] = 5
player.direction = 'right'
elif event.key == pg.K_a:
player.velocity[0] = -5
player.direction = 'left'
elif event.type == pg.KEYUP:
if event.key == pg.K_d and player.velocity[0] > 0:
player.velocity[0] = 0
elif event.key == pg.K_a and player.velocity[0] < 0:
player.velocity[0] = 0
all_sprites.update()
screen.fill((30, 30, 30))
all_sprites.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
main()
pg.quit()