P-232 - Shields++ Details
Python Asteroids+Invaders on GitHub
A meandering trip through changes that didn’t matter until the problem finally became clear to me. A timing bug, one might call it.
Digression
Here begins a fairly long sequence of trying to figure out why the shots fell through the shield when I updated the mask with damage. As I mentioned, the reason is that the shot is checked after the shield (essentially by chance) so the mask has a hole where the shot was, so the shot does not see a collision, so it gets to keep moving, which creates a new collision next time, and it essentially gnaws its way clear through the shield.
What’s below is all the fiddling I did with how to manage the mask and surface. I am keeping it for the record, not because it’s interesting in itself.
I don’t think I have the ideal solution at the end but it’s good enough to commit.
Here we go down a small rat hole
Let’s try this:
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))
surf = self._damage.to_surface()
mask_surf = self._mask.to_surface()
mask_surf.blit(surf, (0, 0))
self._mask = pygame.mask.from_surface(mask_surf)
self.map.blit(surf, (0, 0))
Since the blit worked on the actual shield surface, I imagine that it might work on a surface made from my original mask, so we convert our real mask to a surface, blit it, convert it back. I think I’m seeing black forming around the shields. I change the screen color to light blue to be sure and I get this:
What is that telling me? I thought that black was my surface’s transparent color, so what is going on here? Everything starts out without the black corners. They only appear after the surface is hit the first time. I try resetting the color key to black:
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))
surf = self._damage.to_surface()
mask_surf = self._mask.to_surface()
mask_surf.blit(surf, (0, 0))
self._mask = pygame.mask.from_surface(mask_surf)
self.map.set_colorkey((0, 0, 0))
self.map.blit(surf, (0, 0))
self.map.set_colorkey((0, 0, 0))
Now as soon as they’re hit, the shields turn to white rectangles! I do not understand this.
Let’s see what the mask looks like. I’m going back to the old scheme.
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))
rect = mask.get_rect()
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
# self._mask.set_at((x, y), 0)
self.map.set_at((x, y), "black")
else:
pass
def draw(self, screen):
screen.blit(self.map, self.rect)
mask_rect = self.rect.copy()
mask_rect.centery = self.rect.centery - 96
mask_surf = self._damage.to_surface()
mask_surf.set_colorkey("black")
screen.blit(mask_surf, mask_rect)
With the code like above, with the mask setting commented out, I get this:
The damage mask, shown above each shield, seems to be accumulating damage as expected. What happens if I set the real mask to equal that 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))
rect = mask.get_rect()
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
# self._mask.set_at((x, y), 0)
self.map.set_at((x, y), "black")
else:
pass
self._mask = self._damage
The shots fall right through again!
What am I missing? The same thing happens if I set the mask bits this way:
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))
rect = mask.get_rect()
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
self._mask.set_at((x, y), 0)
self.map.set_at((x, y), "black")
else:
pass
OK, what if I just set a few mask bits directly? I try chopping off the top of the mask. If that works, shots should affect the middle of the shield rather than the top.
class Shield(InvadersFlyer):
def __init__(self, position):
maker = BitmapMaker.instance()
self.map = maker.shield.copy()
self._mask = pygame.mask.from_surface(self.map)
for x in range(0,88):
for y in range(0, 16):
self._mask.set_at((x, y), 0)
self._damage = self._mask.copy()
Yes, that happens. So what’s going wrong in the loop?
I am confused. I carve a dent in the shield mask, in init:
class Shield(InvadersFlyer):
def __init__(self, position):
maker = BitmapMaker.instance()
self.map = maker.shield.copy()
self._mask = pygame.mask.from_surface(self.map)
for x in range(44-16, 44+16):
for y in range(0, 32):
self._mask.set_at((x, y), 0)
self._damage = self._mask.copy()
self._rect = self.map.get_rect()
self._rect.center = position
That works as expected, allowing shots to go deeper in the middle, but none passing through:
What if I dig the hole during the collision, instead of interpreting the other map? That works. The code is this:
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))
for x in range(44-16, 44+16):
for y in range(0, 32):
self._mask.set_at((x, y), 0)
rect = mask.get_rect()
for x in range(rect.right):
for y in range(rect.bottom):
bit = mask.get_at((x, y))
if bit:
# self._mask.set_at((x, y), 0)
self.map.set_at((x, y), "black")
else:
pass
Now I see the same pattern as when I dug the hole in init. The first hit shows a mark on the top of the shield, and the mask gets a hole in it. Thereafter, shots hitting the hold mark deep in the shield. The mask is working for collisions when I clear it as above.
But when I clear it with the bits, bad stuff happens. Let me try putting the range in manually.
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))
for x in range(44-16, 44+16):
for y in range(0, 32):
# self._mask.set_at((x, y), 0)
pass
rect = mask.get_rect()
for x in range(88):
for y in range(64):
bit = mask.get_at((x, y))
if bit:
self._mask.set_at((x, y), 0)
self.map.set_at((x, y), "black")
else:
pass
They fall clear through. Now that would happen if bit was always true.
I put prints in and am certain that only the correct bits are getting set.
The light dawns …
Ohh … wait … maybe we have changed the bits too soon! If we erase those bits and the shot is checked after the shield, it will not see those bits and will continue to fall.
That’s it!! We now return you to your regularly-scheduled article.