First of all, you have to give your sprites a second Rect attribute, the scaled hitbox
which will be used for the collision detection. You have to do that because the self.rect.topleft
coordinates are used as the blit position and if you shrink the Rect, it will be smaller but only fill the upper left area of the image. (The darker area represents the scaled Rect and the brighter area the image.)
________
|░░░░░ |
|░░░░░ |
|░░░░░ |
| |
|________|
So create the hitbox
either by scaling the original Rect with the inflate
method or create a new one with the desired size. I also give my sprites a pos
attribute (a pygame.math.Vector2
) which represents the actual center position of the sprites. When the pos is updated in the update
method, you have to assign it to the center
of the rect and the hitbox as well in order to move them.
Then define a custom collision detection function which will be passed as the collided
argument to pygame.sprite.spritecollide
or groupcollide
. In this function you can use the colliderect
method of the hitbox
Rect to check if it collides with the rect (or hitbox
) of the other sprite.
In your case you also need to shift the hitbox because the weapon of the character is so large. You can do this by defining a vector (called offset
here) which you have to add to the self.hitbox
every update. When the character and its image are flipped, you also have to invert the self.offset
vector.
Here's a minimal, complete example. The images are blue and gray (with the hitboxes centered at the blue area), the hitboxes red and the outer rects (self.rect
for the blitting) are green. Press F to flip the player sprite.
import pygame as pg
from pygame.math import Vector2
pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
pg.draw.rect(IMAGE, (160, 160, 160), (0, 0, 20, 110))
class Entity(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = IMAGE
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.
self.offset = Vector2(10, 0)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
# Add the offset to the pos to center the hitbox
# at the blue part of the image.
self.hitbox.center = self.pos + self.offset
def flip(self):
# Check which side the sprite is facing.
if self.offset.x > 0:
self.image = pg.transform.flip(IMAGE, True, False)
else:
self.image = IMAGE
self.offset = -self.offset # Invert the offset.
# 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."""
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)
player.flip()
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
elif event.type == pg.KEYDOWN:
if event.key == pg.K_f:
player.flip()
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__':
main()
pg.quit()
Alternatively, you could give your sprite two offset vector attributes and add the current offset to the rect.topleft
position when you blit the sprite. However, that means the sprite group's draw
method can't be used anymore and you have to blit the sprites in a for
loop. On the other hand, you wouldn't need the hitbox
and the collided
callback function anymore.
import pygame as pg
from pygame.math import Vector2
pg.init()
IMAGE = pg.Surface((70, 110))
IMAGE.fill((0, 80, 180))
pg.draw.rect(IMAGE, (160, 160, 160), (0, 0, 20, 110))
IMAGE_FLIPPED = pg.transform.flip(IMAGE, True, False)
class Entity(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(*groups)
self.image = IMAGE
self.rect = self.image.get_rect(center=pos).inflate(-40, -20)
self.vel = Vector2(0, 0)
self.pos = Vector2(pos) # Actual center position.
self.offset_right = Vector2(-30, -10)
self.offset_left = Vector2(-10, -10)
self.offset = self.offset_right
def update(self):
self.pos += self.vel
self.rect.center = self.pos
def flip(self):
# Check which side the sprite is facing.
if self.offset.x <= -30:
# Swap the image and the offset vector.
self.image = IMAGE_FLIPPED
self.offset = self.offset_left
else:
self.image = IMAGE
self.offset = self.offset_right
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
all_sprites = pg.sprite.Group()
player = Entity((300, 200), all_sprites)
player.flip()
enemies = pg.sprite.Group(
Entity((100, 250), all_sprites),
Entity((400, 300), all_sprites),
)
# Define blit as 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
elif event.type == pg.KEYDOWN:
if event.key == pg.K_f:
player.flip()
all_sprites.update()
collided_sprites = pg.sprite.spritecollide(player, enemies, False)
for sp in collided_sprites:
print('Collision with id', 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()