Quick answer: Your sprite's rect attribute is almost certainly not being updated when the sprite moves. Collision functions use sprite.rect exclusively for position; if you change self.x or self.y without syncing it to self.rect, collisions check stale coordinates and return empty lists.
Your player sprite clearly walks through an enemy sprite, but pygame.sprite.spritecollide(player, enemies, False) returns an empty list. Your bullet fires straight through a target. Your pickup items never get collected. This is the single most common bug in Pygame's sprite system, and it almost always comes back to the same root cause: Sprite.rect is the truth about a sprite's position, and if you have been updating anything else, you have been invisible to the collision system.
The Symptom
pygame.sprite.spritecollide, pygame.sprite.groupcollide, pygame.sprite.collide_rect, or pygame.sprite.collide_mask returns an empty list (or False) even though you can clearly see two sprites overlapping on screen. Sometimes collisions work early in development and break after you add smooth movement, rotation, scaling, or camera offsets. Sometimes only one direction of collision works — the player hits enemies but enemies do not hit the player.
What Causes This
1. The rect attribute is stale. Pygame's Sprite class has no built-in notion of position. Position is stored in self.rect, a pygame.Rect with integer coordinates. If you store floats in self.x and self.y for smoother movement but forget to copy them to self.rect every frame, the rect never moves. Collision then sees the sprite sitting at its original spawn point forever.
2. Rotation or scaling without rect recalculation. pygame.transform.rotate and pygame.transform.scale return a new surface that is usually larger than the original (to fit the rotated bounds). If you reassign self.image = transform.rotate(...) without recomputing self.rect, your collision bounds stay fixed at the old image size.
3. Groups of the wrong type. pygame.sprite.spritecollide(sprite, group, dokill) expects the group to be a Group (or subclass). Passing a plain list returns nothing without raising an error. Passing a single sprite instead of a group is an equally silent failure.
4. Pixel-perfect collision with no mask. Calling pygame.sprite.collide_mask on sprites that do not have a self.mask attribute returns None (which is falsy). No error, just no collisions.
5. Camera offsets applied to drawing but not to collision. If you use a camera that subtracts an offset when blitting, the sprite appears to move on screen but its rect stays in world space. Collision is correct in world space but does not match what you see.
The Fix
Step 1: Use a proper sprite class that keeps rect in sync.
import pygame
class Player(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.image.load("player.png").convert_alpha()
self.rect = self.image.get_rect(center=(x, y))
# Use floats for smooth movement, sync to rect in update()
self.pos = pygame.math.Vector2(x, y)
self.vel = pygame.math.Vector2(0, 0)
def update(self, dt):
self.pos += self.vel * dt
# CRITICAL: sync float position to the integer rect
self.rect.center = (round(self.pos.x), round(self.pos.y))
This is the canonical pattern. Store the true position as a Vector2 for fractional precision, and mirror it into self.rect.center at the end of every update. The rect stays in sync with the logical position, and collision now works.
Step 2: Recompute rect when the image changes.
def rotate_to_angle(self, angle):
original = self.original_image # Keep the unrotated version
self.image = pygame.transform.rotate(original, angle)
# get_rect() with the current center preserves position
self.rect = self.image.get_rect(center=self.rect.center)
# Update the mask too if using pixel-perfect collision
self.mask = pygame.mask.from_surface(self.image)
Always rotate from the original image, not the already-rotated one. Rotating a rotated image compounds artifacts and rounding errors. Keep self.original_image on the sprite and reassign self.image each time.
Step 3: Verify group types and use the right collision function.
# GOOD: both arguments are Groups
enemies = pygame.sprite.Group()
bullets = pygame.sprite.Group()
hits = pygame.sprite.groupcollide(bullets, enemies, True, False)
for bullet, enemy_list in hits.items():
for enemy in enemy_list:
enemy.take_damage(10)
# BAD: passing a list instead of a Group returns nothing
# hits = pygame.sprite.groupcollide(bullet_list, enemies, True, False)
Step 4: Add masks when using pixel-perfect collision.
class Asteroid(pygame.sprite.Sprite):
def __init__(self, x, y):
super().__init__()
self.image = pygame.image.load("asteroid.png").convert_alpha()
self.rect = self.image.get_rect(center=(x, y))
# Required for pygame.sprite.collide_mask()
self.mask = pygame.mask.from_surface(self.image)
# Use collide_mask as the collided parameter
hits = pygame.sprite.spritecollide(
player, asteroids, False,
collided=pygame.sprite.collide_mask
)
Recompute the mask whenever the sprite's image changes (animation frame, rotation, scaling). A stale mask gives false negatives in the same way a stale rect does.
Step 5: Handle camera offsets correctly.
Keep all collision checks in world space. The camera offset should only apply at draw time:
def draw(self, surface, camera_offset):
for sprite in self.sprites():
draw_rect = sprite.rect.move(-camera_offset.x, -camera_offset.y)
surface.blit(sprite.image, draw_rect)
# Collision still works in world space with original rects
hits = pygame.sprite.spritecollide(player, enemies, False)
Debugging Tip: Draw the Collision Rect
When in doubt, draw the sprite's rect in bright red so you can see exactly where Pygame thinks the sprite is. If the red box is not where the sprite visually appears, you have found your bug:
if DEBUG_DRAW_RECTS:
for sprite in all_sprites:
pygame.draw.rect(screen, (255, 0, 0), sprite.rect, 1)
"The first three hours of every Pygame project are 'why is collision broken?' Answer: you updated self.x and forgot to update self.rect. Move on."
Related Issues
For more Pygame-specific fixes see Pygame music not looping correctly, and for broader performance tuning read Pygame performance tips for indie developers.
sprite.rect is the source of truth. Everything else is decoration.