diff --git a/doc/builtins.rst b/doc/builtins.rst index 4ac0e0fd..f2161ca2 100644 --- a/doc/builtins.rst +++ b/doc/builtins.rst @@ -491,10 +491,17 @@ attribute to some new image name:: Actors have all the same attributes and methods as :ref:`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 '''''''''''''''''' diff --git a/src/pgzero/actor.py b/src/pgzero/actor.py index e8697e9f..863df7bc 100644 --- a/src/pgzero/actor.py +++ b/src/pgzero/actor.py @@ -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): @@ -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): @@ -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) diff --git a/test/test_actor.py b/test/test_actor.py index b74410c4..7adc687a 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -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))