Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion doc/builtins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,17 @@ attribute to some new image name::

Actors have all the same attributes and methods as :ref:`Rect <rect>`,
including methods like `.colliderect()`__ which can be used to test whether
two actors have collided.
two actors have collided. This is quick but imperfect collision detection.

.. __: https://www.pygame.org/docs/ref/rect.html#pygame.Rect.colliderect

Additionally, collisions between actors can be checked more precisely by
calling ``actor1.collidemask(actor2)``. This checks collision down to the
pixel level, meaning that if the rects of two actors overlap but their
images don't actually intersect, a collision won't be reported. This is
a lot more precise but also more work to check for. If coarse detection
is fine, always use ``.colliderect()``. If you need high precision, use
``.collidemask()`` only where it is necessary.

Positioning Actors
''''''''''''''''''
Expand Down
40 changes: 40 additions & 0 deletions src/pgzero/actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,20 @@ class Actor:

def _build_transformed_surf(self):
cache_len = len(self._surface_cache)
# Note if the surface to be displayed has changed.
surf_changed = False
if cache_len == 0:
last = self._orig_surf
else:
last = self._surface_cache[-1]
for f in self.function_order[cache_len:]:
surf_changed = True # We note that we have to change the mask.
new_surf = f(self, last)
self._surface_cache.append(new_surf)
last = new_surf
# If the actor has a mask, it is updated.
if self._mask and surf_changed:
self._mask = pygame.mask.from_surface(self._surface_cache[-1])
return self._surface_cache[-1]

def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, **kwargs):
Expand Down Expand Up @@ -327,6 +333,7 @@ def image(self, image):
self._image_name = image
self._orig_surf = loaders.images.load(image)
self._surface_cache.clear() # Clear out old image's cache.
self._mask = None
self._update_pos()

def _update_pos(self):
Expand Down Expand Up @@ -361,5 +368,38 @@ def distance_to(self, target):
dy = ty - myy
return sqrt(dx * dx + dy * dy)

def _create_mask(self):
"""Gives the actor a mask from the surface that is displayed."""
if not self._surface_cache:
self._mask = pygame.mask.from_surface(self._orig_surf)
else:
self._mask = pygame.mask.from_surface(self._surface_cache[-1])

def collidemask(self, target):
"""Returns True if the actor's mask is colliding with the targets'.
Masks are only created and checked when necessary."""
# Check if the target is an actor and thus suitable.
if not isinstance(target, Actor):
raise TypeError("collidemask() can only be used with other actors,"
"not with a value of type '{}'."
.format(type(target)))

# If the rects don't collide, exit early.
if not self.colliderect(target):
return False

# Create masks that are not yet present.
if not self._mask:
self._create_mask()
if not target._mask:
target._create_mask()

# Calculate the positional offsets of both actors.
x_offset = int(target.left - self.left)
y_offset = int(target.top - self.top)

# Check for pixel perfect collision
return self._mask.overlap(target._mask, (x_offset, y_offset))

def unload_image(self):
loaders.images.unload(self._image_name)
26 changes: 26 additions & 0 deletions test/test_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,29 @@ def test_dir_correct(self):
a = Actor("alien")
for attribute in dir(a):
a.__getattr__(attribute)

def test_mask_collision(self):
"""Collisions are detected with masks in use."""
a1 = Actor("alien")
# For some reason, this is necessary if actors are not drawn but
# collisions should be checked.
a1.pos = (0, 0)
a2 = Actor("alien")
a2.angle = 180
# Since nothing is drawn, the surface has to be updated manually to
# reflect rotation.
a2._build_transformed_surf()
# Collision is detected.
self.assertIsNotNone(a1.collidemask(a2))

def test_mask_no_collision(self):
"""Even if rects overlap, masks correctly report no collision if no
pixels overlap."""
a1 = Actor("alien")
a1.pos = (0, 0)
a2 = Actor("alien")
a2.angle = 180
a2._build_transformed_surf()
a2.pos = (10, 87)
# No collision is detected.
self.assertIsNone(a1.collidemask(a2))