Quick answer: Plain pygame.sprite.Group has no draw order guarantees. Use pygame.sprite.LayeredUpdates instead, set each sprite’s _layer attribute, and call group.change_layer when sprites move. For Y-sort top-down games, set _layer = rect.bottom each frame.
Here is how to fix Pygame sprite groups whose draw order seems random. Your player walks behind a tree on one frame and in front of it the next. Or the HUD bar draws below the world. The default Group class makes no guarantees about draw order, and the workaround is to use LayeredUpdates with explicit per-sprite layer values.
The Symptom
You add sprites to a pygame.sprite.Group and call group.draw(screen). The player draws above some tiles but below others. The order changes when sprites are added or removed. You try sorting the group with sorted(group.sprites(), ...) but the next call to draw ignores the sort.
What Causes This
Group does not preserve insertion order across versions. The implementation uses a dict internally, and while modern Python preserves dict insertion order, the practical iteration during draw was never specified to be stable across pygame versions.
No layer concept in Group. A plain Group has no per-sprite priority. There is no way to say “always draw the player last.”
Sorting the sprite list does not persist. Methods like group.sprites() return a list copy. Sorting it does not change the underlying group iteration order.
Y-sort needs frame updates. Even with a layered group, Y-sorting requires updating each sprite’s layer every frame to reflect its current Y position.
The Fix
Step 1: Use LayeredUpdates for ordered groups.
import pygame
class Tile(pygame.sprite.Sprite):
def __init__(self, image, pos):
super().__init__()
self.image = image
self.rect = image.get_rect(topleft=pos)
self._layer = 0 # background
class Player(pygame.sprite.Sprite):
def __init__(self, image, pos):
super().__init__()
self.image = image
self.rect = image.get_rect(center=pos)
self._layer = 10 # draws on top
world = pygame.sprite.LayeredUpdates()
world.add(tile1, tile2, player)
world.draw(screen)
Step 2: Y-sort for pseudo-depth in top-down games.
def update_y_sort(group):
for sprite in group:
# Use rect.bottom so feet line up with depth
new_layer = sprite.rect.bottom
if sprite._layer != new_layer:
group.change_layer(sprite, new_layer)
# Each frame
update_y_sort(world)
world.draw(screen)
Step 3: Reserve high layer numbers for HUD. If you draw the HUD through the same group, give HUD sprites a layer like 1000 so they always draw above any Y-sorted world content.
health_bar._layer = 1000
world.add(health_bar)
Step 4: Avoid mutating layers mid-iteration. Update layers before calling draw, never inside a draw loop. Modifying a LayeredUpdates group during iteration can produce inconsistent results.
Step 5: Use multiple groups for clarity. Separate groups for background tiles, world entities, and HUD. Draw each in turn:
background_group.draw(screen)
entity_group.draw(screen) # LayeredUpdates with Y-sort
foreground_group.draw(screen)
hud_group.draw(screen)
Performance Note
LayeredUpdates is slightly slower than plain Group because it sorts on every layer change. For Y-sort with hundreds of sprites, set the layer only when the sprite’s Y actually changes:
def update(self, dt):
old_y = self.rect.bottom
# ... move sprite ...
if self.rect.bottom != old_y:
self.groups()[0].change_layer(self, self.rect.bottom)
“Group has no opinion about order. LayeredUpdates does. The _layer attribute is the contract between you and pygame.”
Related Issues
For other Pygame issues, see Pygame Joystick Not Detected.
LayeredUpdates. _layer attribute. change_layer when it moves. Y-sort comes for free.