Everyone needs them. Let’s make that happen. Then, let’s explode some invaders.
I was thinking about rects, [bit]maps and masks, because I need maps to draw, rects to draw them in, rects and masks to check collisions. And this morning, I was wondering whether the game should create the masks right along with creating the maps. And this morning, I was wondering how to ensure that all the invaders game objects have a mask to collide, and I probably should have been thinking about how to ensure that they have a rect as well.
I’ve mostly started keeping track of where things are in a Rect object, like this:
class Invader: def __init__(self, column, row, bitmaps): self.bitmaps = bitmaps self.masks = [pygame.mask.from_surface(bitmap) for bitmap in self.bitmaps] self.column = column self.relative_position = Vector2(INVADER_SPACING * column, -INVADER_SPACING * row) self.rect = pygame.Rect(0, 0, 64, 32) self.image = 0 @property def position(self): return Vector2(self.rect.center) @position.setter def position(self, vector): self.image = 1 if self.image == 0 else 0 self.rect.center = vector + self.relative_position
I get the masks for an invader as you see above, which means that each invader has his own version of the masks, which will be just like the masks of all the other invaders of his particular style. That’s a bit wasteful.
You’ll see that by the time I get to the end of this, I have completely forgotten about the duplication. I had been thinking about a little object to hold bitmap and mask … and it completely slipped my mind in all the excitement.
The objects don’t all work just like that, but similarly.
class InvaderPlayer(InvadersFlyer): def __init__(self): maker = BitmapMaker.instance() self.players = maker.players # one turret, two explosions self.player = self.players self.rect = self.player.get_rect() self.rect.center = Vector2(u.SCREEN_SIZE/2, u.SCREEN_SIZE - 5*32 - 16) self.step = 4 half_width = self.rect.width / 2 self.left = 64 + half_width self.right = 960 - half_width self.free_to_fire = True self.fire_request_allowed = True
Player does not as yet have a mask. Let’s right now add mask and rect as properties that an InvadersFlyer must have. We’ll see what happens. I think we say it like this:
class InvadersFlyer(Flyer): @property @abstractmethod def mask(self): pass @property @abstractmethod def rect(self): pass
This breaks something like 24 tests.
E TypeError: Can't instantiate abstract class InvaderShot with abstract method rect
Perfect, just what I wanted. It appears, however, that Python will not recognize a simple member as satisfying those abstract methods. This seems to work:
class InvaderShot(InvadersFlyer): def __init__(self, position, maps): self.maps = maps self.masks = [pygame.mask.from_surface(bitmap) for bitmap in self.maps] self.map = maps self.map_index = 0 self._rect = self.map.get_rect() self.rect.center = position self.count = 00 @property def rect(self): return self._rect @property def mask(self): return self.masks[self.map_index]
I am OK with that, I think. We’ll push on and see what we think when all the tests are happy again.
In InvaderFleet, I just return None for both. I am not sure whether I can get away with that but the tests improved. Bumper, similarly. TopBumper.
PlayerShot needs real ones. I do the same trick with making the members private and overloading the
rect methods. ShotExplosion also. Etc.
That’s all done, all the same way, either setting the mask and rect into private members and then implementing the properties, or just returning None for objects that have no visible appearance, like the bumpers.
I think that’s OK. tests are green, game works as well as ever. Commit: make mask and rect required properties in InvadersFlyer subclasses. Can be and are dynamic in animated invaders.
Another irritating update of 8 classes, but I think it makes things better. There is a lot of very tiny duplication going on here, though. I’m not sure what should be done about that.
Let’s do the invader explosion. It’s a single frame thing. How does ShotExplosion work?
class ShotExplosion(InvadersFlyer): def __init__(self, position): self.position = position maker = BitmapMaker() self.image = maker.player_shot_explosion self._mask = pygame.mask.from_surface(self.image) self._rect = self.image.get_rect() self.image.fill("red",self.rect, special_flags=pygame.BLEND_MULT) self.time = 0.125 @property def mask(self): return self._mask @property def rect(self): return self._rect def tick(self, delta_time, fleets): self.time -= delta_time if self.time < 0: fleets.remove(self)
Let’s do InvaderExplosion similarly. I’ll create a new file, having at least somewhat learned my lesson.
I literally paste the whole ShotExplosion class in there and rename it. I’ll show the init in a moment, but all I really did was fetch the
invader_explosion bitmap instead of the shot explosion.
Now, however, the Invader, upon being destroyed, needs to create an explosion and put it into Fleets. And it does not have access to the Fleets instance:
def interact_with_playershot(self, shot, group): if self.colliding(shot): shot.hit_invader() group.kill(self)
Maybe the group can handle this. Not remotely:
class InvaderGroup: def kill(self, invader): index = self.invaders.index(invader) self.invaders.pop(index) if self._next_invader > index: self._next_invader -= 1
The group does not know the InvaderFleet, and the InvaderFleet does not know fleets except when it’s being called for an interaction:
class InvaderFleet(InvadersFlyer): def interact_with_playershot(self, shot, fleets): self.invader_group.interact_with_playershot(shot)
We can pass the fleets instance in on the interact. Let’s do that and see if we can hook it up.
class InvaderFleet(InvadersFlyer): def interact_with_playershot(self, shot, fleets): self.invader_group.interact_with_playershot(shot, fleets) class InvaderGroup: def interact_with_playershot(self, shot, fleets): for invader in self.invaders.copy(): invader.interact_with_playershot(shot, self, fleets) class Invader: def interact_with_playershot(self, shot, group, fleets): if self.colliding(shot): shot.hit_invader() group.kill(self) fleets.append(InvaderExplosion(self.position))
That might just do the thing. Tests are broken, though. It’s the one that reminds me that everyone has to
interact_with_invaderexplosion now. This is tedious in the extreme. I implement it concretely in InvadersFlyer.
Insufficient effort. I have to change all the
interact_with_playershot to expect that fourth parameter.
This is a bit off. Let’s make it work, then make it right.
I change all those to expect another parameter, and the good news is that it works:
The bad news is that I think I did a bad thing. The general protocol of
interact_with_object takes three parameters,
fleets. I’ve changed InvadersFlyer to have one of those with four parameters instead of three. I think that’s not OK.
We should have a special method that we send to the group.
I’m going to go back to InvadersFlyer, undo the extra parameter, and fix it to work.
class InvadersFlyer(Flyer): @abstractmethod def interact_with_playershot(self, shot, fleets): pass
One test fails. The refactoring tool left a parameter out. Replaced and we are OK. Let’s track down now from InvaderFleet.
class InvaderFleet(InvadersFlyer): def interact_with_playershot(self, shot, fleets): self.invader_group.interact_with_playershot(shot, fleets) class InvaderGroup: def interact_with_playershot(self, shot, fleets): for invader in self.invaders.copy(): invader.interact_with_playershot(shot, fleets) class Invader: def interact_with_playershot(self, shot, group, fleets): if self.colliding(shot): shot.hit_invader() group.kill(self) fleets.append(InvaderExplosion(self.position))
This does not line up. Let’s rename that one method.
class InvaderGroup: def interact_with_playershot(self, shot, fleets): for invader in self.invaders.copy(): invader.interact_with_group_and_playershot(shot, self, fleets) class Invader: def interact_with_group_and_playershot(self, shot, group, fleets): if self.colliding(shot): shot.hit_invader() group.kill(self) fleets.append(InvaderExplosion(self.position))
Works and preserves the overall pattern. Commit: Invaders hit by player shots now display the invader explosion pattern for 0.125 seconds.
Let’s sum up.
We improved the overall design, I think, by requiring properties
mask on all the invaders objects. I have a mild concern over the ones returning None, we might do better to return an empty rect and an empty mask rather than None, but so far they have not caused problems.
However, if anyone ever did try to check collisions on them, bad things would happen.
I’m not up for fixing them right now: I want to do some testing on empty rectangles and such. We can do this, however:
class Collider: 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
Now if any
None creep in, we’ll report no collision. I don’t like checking parameters like that, but it’s safe now.
Then we implemented ShotExplosion. I made a mistake by adding an extra parameter to a well-known method
interact_with_playershot but changed it back by providing a special entry point for the group and invader to use instead. No harm done, and it wasn’t really bad the other way, just potentially confusing, since all the other
interact_with_ methods don’t have that parameter.
I dealt with the need to implement the [REDACTED] method in all kinds of classes by implementing it as non-abstract up in InvadersFlyer. Sorry / not sorry. There must be a better solution than the two that I see now, making it abstract and editing more and more classes to add it, and making it concrete and inheriting concrete behavior, with a small chance that I’ll forget to override the method where it’s needed. I have not made that mistake … lately.
Anyway, we have a nice explosion now. Next time, maybe, we’ll start the invader shots according to the original game’s scheme. That will be amusing, I think.
See you next time!