Python 218 - Collision 2
Python Asteroids+Invaders on GitHub
At least two collision issues face me. One is how to stop the shot after it hits an invader. Another is that I see an object trying to be born.
Let’s start by stopping the shot after it hits an invader. I think a callback to it is the right answer. My thought is to set a flag during begin_interactions
, clear it by callback if the shot should stop, and check it in end_interactions
.
Let me just code that, please.
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(2, -8*4)
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
self.bits = maker.player_shot
self.mask = pygame.mask.from_surface(self.bits)
self.rect = self.bits.get_rect()
self.position = position + offset
self.should_die = False
def begin_interactions(self, fleets):
self.should_die = False
def hit_invader(self):
self.should_die = True
def end_interactions(self, fleets):
if self.should_die:
fleets.remove(self)
I think that would just work if we were to do this:
class Invader:
def interact_with_playershot(self, shot, group):
if self.colliding(shot):
shot.hit_invader()
group.kill(self)
That works as intended. As soon as the shot hits one invader, that invader disappears, and the shot disappears as well.
Commit: Shot dies after hitting only one invader.
Should I have done a test? It’s never easy to argue against a test, but that was so straightforward. Let’s think about how we could test it. One way would be like this:
def test_shot_removes_itself(self):
fleets = Fleets()
fi = FI(fleets)
shot = PlayerShot()
fleets.append(shot)
assert fi.player_shots
fleets.begin_interactions()
shot.hit_invader()
fleets.end_interactions()
assert not fi.player_shots
I think that’s rather good. Commit: test for shot removal after being told hit_invader
.
We could do more, but I think that does the main thing. It might be better to push through an invader to be sure we didn’t leave that line out. We’d have to set up a group and such.
Am I a bad person for not doing that? Perhaps. But I’ve done worse things.
Let’s talk about the object I feel trying to be born.
Our code for detecting a shot vs invader collision looks like this:
class Invader:
def interact_with_playershot(self, shot, group):
if self.colliding(shot):
shot.hit_invader()
group.kill(self)
def colliding(self, invaders_flyer):
return self.rectangles_overlap(invaders_flyer) and self.masks_overlap(invaders_flyer)
def masks_overlap(self, invaders_flyer):
return self.mask.overlap(invaders_flyer.mask, self.mask_offset(invaders_flyer))
def mask_offset(self, invaders_flyer):
return Vector2(invaders_flyer.rect.topleft) - Vector2(self.rect.topleft)
def rectangles_overlap(self, shot):
return self.rect.colliderect(shot.rect)
The collision logic here is one-sided, because of the odd way that invaders are treated as a group most of the time. So the shot will never see an interact
event from the invader, which is why the most expeditious thing seemed to be to just send it a message telling it that it hit an invader.
The other forms of collision we have will all be symmetric, invader shots vs shield and player. But the basic checking will be the same as we have here, first the rectangles and then the masks.
In addition, collision is a two-object sort of thing, not a one-sided thing. Speculating about player and shield, we can imagine that they would each have similar code to the code here in Invader. In fact, it would likely be identical, and at least it seems likely that it would be just the same between shield and player. Which would tempt one to inherit it from above.
But Invader has no superclass from which to inherit, even if we were so horrible as to allow implementation inheritance. And of course “we all know” that delegation is to be preferred rather than inheritance.
So I think there is a CollisionChecker trying to be born, which is created with two objects and answers whether they are colliding, by checking their rectangles and masks. It follows that objects that are sent to the CollisionChecker would need to support some minimal interface giving access to the rectangle and mask for the object. (Alternatively, the CC could create the mask given the surface, but that seems inefficient. Not that we are deeply concerned about that.)
It occurs to me that we should probably produce the mask as part of the BitmapMaker, because it is wasteful to create one or more masks in every occurrence of the Invader class. Better to just make them once and pass them to everyone.
Anyway, there’s some minimal interface that we would probably want to create and make abstract so that Invader and our Player and Shield could inherit it.
I want to let that idea perk a bit before I go after it, and possibly I should wait until I’m doing invader shots against player before I work on it. It might be premature to generalize it this soon. In any case, that’s not for today.
For today, we have a player shot that detects collisions including use of bitmap masks, and that’s a nice place to stop. We even have a bunch of tests around it. Could be better … but pretty darn good. Satisfying, I’d say.
See you next time!