There are two, relatively clean, solutions to this problem:
Either give your sprite a separate hitbox
attribute (the scaled rect) and pass a customized callback function to pygame.sprite.spritecollide
in which you check if the hitboxes instead of the rects collide, or alternatively scale the actual self.rect
, then blit the sprites in a for loop and add a predefined offset vector to the rect.topleft
coordinates to adjust the blit position.
Here's the hitbox
solution:
import pygame as pg
from pygame.math import Vector2
pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
class Entity(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = IMAGE
# The rect is still used as the blit position.
self.rect = self.image.get_rect(center=pos)
# A deflated copy of the rect as the hitbox.
self.hitbox = self.rect.inflate(-40, -20)
self.vel = Vector2(0, 0)
self.pos = Vector2(pos) # Actual center position.
def update(self):
# Move the sprite by adding the velocity vector to the position.
self.pos += self.vel
self.rect.center = self.pos # Update the rect.
# Move the hitbox as well. You can also adjust the position
# of the hitbox here by adding another vector to the pos.
self.hitbox.center = self.pos
# This callback function is passed as the `collided`argument
# to pygame.sprite.spritecollide or groupcollide.
def collided(sprite, other):
"""Check if the hitboxes of the two sprites collide."""
# Check if the hitboxes collide (instead of the rects).
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 = Entity((300, 200), all_sprites)
enemies = pg.sprite.Group(
Entity((100, 250), all_sprites),
Entity((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 with sprite', id(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__':
main()
pg.quit()
And the offset vector solution:
import pygame as pg
from pygame.math import Vector2
pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
class Entity(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = IMAGE
# Scale the rect with the inflate method.
self.rect = self.image.get_rect(center=pos).inflate(-40, -20)
self.vel = Vector2(0, 0)
self.pos = Vector2(pos) # Actual center position.
# Add this vector to the rect.topleft coords during the
# blitting to adjust the position.
self.offset = Vector2(-20, -10)
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 = Entity((300, 200), all_sprites)
enemies = pg.sprite.Group(
Entity((100, 250), all_sprites),
Entity((400, 300), all_sprites),
)
# Assign screen.blit to a local variable to increase the performance.
blit = screen.blit
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()
collided_sprites = pg.sprite.spritecollide(player, enemies, False)
for sp in collided_sprites:
print('Collision with sprite', id(sp))
screen.fill((30, 30, 30))
for sprite in all_sprites:
# Add the current offset to the blit position.
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__':
main()
pg.quit()