P-232 - Shields++
Python Asteroids+Invaders on GitHub
It’s 3 AM. The power’s out, the phones are down. Of course I’m going to program a bit.
I just happened to wake up, thought I’d check the outage status. Now I’m slightly awake, so I guess I’ll work on the interaction between player shot and shield. Just the shot part, where it dies.
I just replicate our same old test for the PlayerShot:
def test_playershot_dies_on_shield(self):
fleets = Fleets()
fi = FI(fleets)
pos = Vector2(100, 100)
shield = Shield(pos)
maker = BitmapMaker.instance()
shot = PlayerShot(pos)
assert shot.colliding(shield)
fleets.append(shot)
assert fi.player_shots
shot.interact_with_shield(shield, fleets)
assert not fi.player_shots
Fails on the last line, no surprise.
class PlayerShot(InvadersFlyer):
def interact_with_shield(self, shield, fleets):
if self.colliding(shield):
fleets.remove(self)
Test is green. Test in game just for fun. Works, of course. Commit: player shot dies on shield.
Can’t push, cable is out. Could set up a wifi hot spot but life’s too short. I hope we get power, phone, and cable back soon. We’ve never lost the phone line before. Fortunately, climate change is a hoax.
What does our story list look like now?
-
Get one shield on the screen; -
Get all four on the screen; -
Shots ignore shields; -
InvaderShots die upon hitting shields; -
PlayerShots die upon hitting shields; - InvaderShots do simple damage;
- PlayerShots do simple damage;
- InvaderShots do fancy damage;
- PlayerShots do fancy damage;
Ah. We’re left with damage to the shield. That should be interesting. I still haven’t looked up the pygame stuff. Let’s review the Collider as a start:
class Collider:
def __init__(self, left, right):
self.left_rect = left.rect
self.left_mask = left.mask
self.right_rect = right.rect
self.right_mask = right.mask
def colliding(self):
if self.right_rect and self.right_rect and self.left_mask and self.right_mask:
return self.rectangles_colliding() and self.masks_colliding()
else:
return False
def rectangles_colliding(self):
return self.left_rect.colliderect(self.right_rect)
def masks_colliding(self):
offset = Vector2(self.right_rect.topleft) - Vector2(self.left_rect.topleft)
return self.left_mask.overlap(self.right_mask, offset)
So, right. If the rects are colliding and the masks are colliding (i.e. masks overlap), we are colliding. What else do masks know how to do? Oh my … I can’t hit up the pygame site: no internet. That makes this a bit more challenging.
PyCharm can provide me some stub info. It doesn’t have the pygame source at hand, I guess. I wonder what it does have? Anyway:
def overlap_mask(self, other, offset): # real signature unknown; restored from __doc__
"""
overlap_mask(other, offset) -> Mask
Returns a mask of the overlapping set bits
"""
return Mask
def erase(self, other, offset): # real signature unknown; restored from __doc__
"""
erase(other, offset) -> None
Erases a mask from another
"""
pass
def to_surface(self): # real signature unknown; restored from __doc__
"""
to_surface() -> Surface
to_surface(surface=None, setsurface=None, unsetsurface=None, setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 255), dest=(0, 0)) -> Surface
Returns a surface with the mask drawn on it
"""
pass
Those look interesting. I’m thinking that our job, showing shield damage, comes in two parts: erase some pattern on the surface that displays, and erase the same pattern on the mask, so that the next collision will go deeper into the shield.
Let’s spike some code into Shield, to see what we can glean from the method prompts:
class Shield(InvadersFlyer):
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
mask = collider.overlap_mask()
class Collider:
def overlap_mask(self):
offset = Vector2(self.right_rect.topleft) - Vector2(self.left_rect.topleft)
return self.left_mask.overlap_mask(self.right_mask, offset)
Just totally guessing at that. Let’s add a print to the Shield and run it. It does print and does not explode. The print is uninformative: <Mask(88x64)>
, but seems credible.
Let’s look for methods in Surface, maybe we can use the overlap mask directly. I don’t find anything promising. Without documents, I have little in the way of resources. But I think I do know how to iterate and access bits.
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
mask: Mask = collider.overlap_mask()
rect = mask.get_rect()
print("begin")
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
print("*", end="")
else:
print("_", end="")
print()
print("complete")
That should print the bitmap. And, somewhat surprisingly, it does. For example:
________________****____****____________________________________
________________****____****____________________________________
________________****____****____________________________________
________________****____****____________________________________
____________****________________________________________________
____________****________________________________________________
____________****________________________________________________
____________****________________________________________________
________________________________________________________________
________________________________________________________________
...
complete
So what if I were to erase the surface where the bit is one?
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
mask: Mask = collider.overlap_mask()
rect = mask.get_rect()
map = self.map
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
map.set_at((x, y), "black")
else:
pass
Delightfully, that sort of works. After running for a bit:
Friends, we have damage!
It’s not reflected in the mask but we’ve shown that we can detect the overlap. From here, it’s just tuning. (Well, OK, perhaps some heavy tuning, but this is very promising.)
Let’s reflect, save this stuff, and go back to bed.
Reflection
I’m rather happy with this. Perhaps there’s a better way to do it, that I can discover when I can read the pygame documents again, but using just the hints from PyCharm and what I know about setting bitmaps, we’ve shown that we can detect where the shield is hit and smush in some damage there. Now, I think, its mostly a matter of deciding what shape the damage should be. Using the shape of the shot makes some sense, but it’s pretty light damage. We can experiment with that, maybe use a little explosion shape, or one of several. I’m not sure what the original game does, we might look at that as well. I have a vague recollection that it uses the image of the shot somehow, but I really don’t remember.
But this is nice. And I’m going back to bed: it’s only 0420 here chez Ron. See you later, I hope!
Later …
Not as later as I’d wish, but there you have it. I’ve tried a different approach, using pygame pages I managed to load via cellular data. (That would work better if we had decent cellular coverage here, but that is not the case.)
I’ve added a second mask, _damage
, initially a copy of the shield’s original, and I use it like this:
class Shield(InvadersFlyer):
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
mask: Mask = collider.overlap_mask()
self._damage.erase(mask, (0, 0))
map = self.map
surf = self._damage.to_surface()
map.blit(surf, (0, 0))
That gives me the same damage effect on screen as we see above. Which was what I intended.
But I want to use the damage on the real mask, so that it will be used in subsequent collisions, allowing the damage to eat deeper into the shield. I was trying this earlier and had trouble. Let’s try again.
Mask knows erase, as we see above. What happens if we erase into our existing mask?
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
mask: Mask = collider.overlap_mask()
self._damage.erase(mask, (0, 0))
self._mask.erase(mask, (0, 0))
surf = self._damage.to_surface()
self.map.blit(surf, (0, 0))
What happens is that the shots now travel clear through the shield, as if the mask is now all zero.
Long Digression
I’ll put the details here. I learn a lot about masks and such but the bug occurs because if I damage the actual mask during interactions, and if the shot is checked after the shield, which it is, the shot sees the hole in the shield and continues down, making a new hole, which it sees …
Welcome Back
Fascinating! We need to give the shot time to die. Let’s cache the map and set it on begin_interactions. This is getting weird.
That works, with one exception, which is that the damage is being set to black. I thought our color key was black. I set it in init and now the damage is transparent, showing the background. Puzzle solved! Fascinating!
I think I can commit this: Shields take damage from invader shots and mask is updated accordingly.
Summary
I think I should do the mask update in end_interactions
, not begin_interactions
. And I can get rid of the extra _damage
mask. And I can probably go back to doing it with blit rather than with my loops. But all that’s for next time. For this time, it’s good enough.
But it’s an interesting timing problem. I don’t think I’ve encountered one like it with this design. Probably should think about it a bit, at some future time when I can think.
See you next time!