P-269 - Bottom Line
Python Asteroids+Invaders on GitHub
Yeah, let’s do the bottom line. Might be just the right size for the afternoon.
In the original game, a line across the bottom of the screen took damage when invader shots hit it. Kind of a nice effect, better than having them just vanish. Let’s see about doing that. I can think of two sources for implementation ideas: The shields take damage when hit, and we have side and top bumpers. We could have one on the bottom as well.
Invader shots presently move like this:
def move(self, fleets):
self.moves += 1
self.update_map()
self.position = self.position + Vector2(0, 16)
if self.position.y >= u.SCREEN_SIZE:
self.die(fleets)
And they interact with the Shield like this:
def interact_with_shield(self, shield, fleets):
self.die_on_collision(shield, fleets)
The Shield, on the other hand, does this:
def interact_with_invadershot(self, shot, fleets):
self.process_shot_collision(shot, self._invader_shot_explosion, self._invader_explosion_mask)
def process_shot_collision(self, shot, explosion, explosion_mask):
collider = Collider(self, shot)
if collider.colliding():
self._tasks.remind_me(lambda: self.mash_image(shot))
def mash_image(self, shot):
masher = ImageMasher.from_flyers(self, shot)
masher.determine_damage()
self._mask = masher.get_new_mask()
masher.apply_damage_to_surface(self._map)
Shall we review ImageMasher? Sure. It might be enlightening. But there’s a lot to it:
class ImageMasher:
@classmethod
def from_flyers(cls, target, shot):
target_masker = Masker(target.mask, target.position)
shot_masker = Masker(shot.mask, shot.position)
explosion_masker = Masker(shot.explosion_mask, shot.position)
return cls(target_masker, shot_masker, explosion_masker)
def __init__(self, target_masker, shot_masker, explosion_masker):
self.target_masker = target_masker
self.shot_masker = shot_masker
self.explosion_masker = explosion_masker
def determine_damage(self):
self.erase_shot()
self.erase_explosion()
def erase_shot(self):
self.target_masker.erase(self.shot_masker)
def erase_explosion(self):
self.target_masker.erase(self.explosion_masker)
def get_new_mask(self):
return self.target_masker.mask
def apply_damage_to_surface(self, surface):
new_mask = self.get_new_mask()
area_to_blit = new_mask.get_rect()
damaged_surface = new_mask.to_surface()
surface.blit(damaged_surface, area_to_blit)
Let’s see what we can figure out without looking at Masker. The ImageMasher deals with three maskers, the target, which I guess will be our line, the shot, which will be, well, the shot, and the explosion masker, the mask of the explosion. Its usual thing is to erase both the shot mask and the explosion mask from the target. That’s what makes the large ragged hole in the Shield when a shot hits it. I think we’ll leave open that the shot explosion will be used to mark up the line as well. If we don’t like the effect, we’ll deal with it then.
I am inclined to think that if we just set up a new object, the BottomLine and run this code on it, good things might happen.
What tests do we have for Shield vs Shot, if any?
I LOL. Here they are:
class TestShield:
def test_exists(self):
Shield(Vector2(0, 0))
@pytest.mark.skip(reason="needs work")
def test_mask_updates_after_shield_hit(self):
pass
@pytest.mark.skip(reason="good learning experience")
def test_erase_and_blit_to_show_how_they_work_in_shield(self):
pass
Left for later and later has never come.
Let’s try to do a test for the bottom line.
class BottomLine:
pass
class TestBottomLine:
def test_exists(self):
BottomLine()
So far so good. Commit: BottomLine class being built in tests.
What does it do? From pictures of the original game, it draws a green line just above the spare player icons.
Let’s do that and get some numbers out of the deal. A bit of hacking and:
def draw(self, screen):
y = u.SCREEN_SIZE - 56
pygame.draw.rect(screen, "green", (64,y, 960-64, 4))
With this in place I think I can build the proper rectangle and mask and use them.
class BottomLine(InvadersFlyer):
@property
def mask(self):
return None
@property
def rect(self):
y = u.SCREEN_SIZE - 56
return Rect(64, y, 960-64, 4)
def draw(self, screen):
pygame.draw.rect(screen, "green", self.rect)
I’m kind of inching up on this. Let’s commit: inching up on bottomline.
I need a bitmap here, and a mask made from it. A bit more reading and fiddling and I have this:
class BottomLine(InvadersFlyer):
def __init__(self):
w = 960-128
h = 4
surface = Surface((w, h))
surface.set_colorkey((0, 0, 0))
surface.fill("green")
rect = Rect(0, 0, w, h)
rect.bottomleft = (64, u.SCREEN_SIZE - 56)
self._rect = rect
self.map = surface
self._mask = pygame.mask.from_surface(surface)
@property
def mask(self):
return self._mask
@property
def rect(self):
return self._rect
def draw(self, screen):
screen.blit(self.map, self.rect)
Same picture. Now we have a real bitmap and mask. Commit: bottomline has bitmap and mask.
I am so tempted just to replicate the shield damage code here. Can you blame me?
I decide to try it, just a simple copy-pasta job. I get a working but undesirable result. My nice green line turns white.
Now the truth is, the shields are supposed to be green as well. I have not figured out how to mask a surface retaining the color. Reading pygame, it’s as if no one ever wanted to do this before.
It will take me some time to figure that out, but for now, we have a working, if unattractive, implementation.
Commit: bottom line takes damage but turns white.
Summary
If I were a different kind of person, I would consider this to be a spike and throw it away. For sure I have a lot of code duplicated between Shield and BottomLine now. But I am more inclined to consider this a step in roughly the right direction, and it does show progress, so I’ll allow it.
But I really need to figure out how to apply a mask to a colored surface. This will take some reading and experimentation.
See you next time!