Python 216 - Darlings
Python Asteroids+Invaders on GitHub
In response to a question from Bruce, I find myself looking askance at one of my darlings. Should I kill it?
Bruce Onder noticed that in the current very incomplete implementation of the player shot, the shot code just checks to see if it is off screen and removes the shot, allowing another shot to be fired. He asked whether I had considered using a top bumper to trigger the shot’s final actions.
As it happens, I had not considered that. It’s too early in the shot’s evolution to have considered it, I think, since I was just bashing in a little motion to get things started. But I suspect that I would not have thought of it, because once we have code in the shot that considers its position, there wouldn’t be much pressure to change it to run on a bumper-like interaction. Here’s part of our exchange:
Ron:
top might make sense, if we assume the sides do. Which I doubt, frankly. Bottom, similar, I guess.
Bruce:
wait, the side bumpers don’t make sense?
Ron:
well, their function could be accomplished with a simple if statement …
I’m wondering if I love this weird design too much. 😀
Bruce:
I think there is value in taking full advantage of the concept of decentralization. Maybe that’s love, or maybe it’s just opinionated design.
So let’s think about this a bit.
The fundamental idea of this “decentralized” design is that the objects in the game are not linked together in any meaningful way. The top levels only see a plain typeless collection and (mostly do not hold on to any particular instances or think about types. The individual instances (mostly) do not hold on to their creators or their children.
Instead, during each cycle, every object gets a chance to interact with every other object. I think of it as a bit like an event-driven system, although the implementation really is that each object is sent a message offering interaction with every other object. The receiver can do whatever it wants with that message. It might check distances and decide that it has been killed. It might notice that it has not seen any asteroids, and decide to create some. The individual objects pay attention to anything they care about and take whatever action is appropriate to their interests and to the game.
I have written before to the effect that my friends, some of whom seem otherwise quite intelligent, find this design difficult to think about. I have come to agree with them. It is tricky to think about, in the same way that the universe is tricky to think about, because we very often have to consider what each individual will do, and we have to observe a lot to make our own decisions.
Think about driving, if you happen to drive. We have to watch all the other vehicles, the road signs, the traffic signals, the turkeys crossing the road, the deer that bounds across right in front of us, the school bus with flashing lights, the idiot on the motorcycle … and many of those objects are watching us and changing what they do based on what they see. The squirrel that has almost safely reached the other side decides to turn around and run back across the road. Noooo!
Life is hard to think about sometimes, and, to a lesser degree, so is my little game program. When we program, we generally try to create a program that is not hard to think about. Because of the characteristics of the problem we may not be able to use as simple a solution as we’d like, but if we’re wise, we keep things as simple as we can.1
In a game as small as Asteroids, a fairly simple approach is possible. The original game only considered certain interactions, and it kept track of some key information at the top of the program. It never considered two asteroids interacting, because the game design had decided that they never interact. So the code takes advantage of that fact.
The decentralized design used in the version we’re working on now has thrown away that kind of thinking, and clearly, for the game of Asteroids, that is not necessary. We know that it has been done well using more detailed specialized knowledge.
I don’t remember how the decentralized notion came to me, but it seemed interesting, so I did it. I find it reminiscent of an event-driven system, or an “actors” kind of design where all the objects are independent active objects. It’s more than we need for asteroids.
But we are engaged in an interesting enterprise right now. By creating a few new kinds of objects, so far, an invader fleet, invader objects, bumpers, and a player and its shot, we are creating an entirely different game that is running in the very same program.
Type “q and you are playing Asteroids. Type “i” and you are playing invaders. That’s rather nifty, I think.
However, it does come at a cost, in that we have to think in the special terms that this design provides. Or do we?
Clearly, I can make the PlayerShot get near the top of the screen and do its explosion trick with code right in PlayerShot. We already have this:
class PlayerShot(InvadersFlyer):
def update(self, delta_time, fleets):
self.position += self.velocity
if self.position.y <= 0:
fleets.remove(self)
If we have to explode for a while, we can clearly code it right in there. Or we could create a PlayerShotExplosion instance and toss it into the mix. We don’t need a bumper to tell us to do that.
On the other hand, we could, given a TopBUmper instance at the top of the screen, have coded something like this:
class PlayerShot(InvadersFlyer):
def update(self, delta_time, fleets):
self.position += self.velocity
def interact_with_topbumper(self, bumper, fleets):
if self.has_hit(bumper):
fleets.remove(self)
Is that better? It’s more consistent with our overall design, as Bruce suggests. Once we understand that design, the second version is a bit more explicit in that we see more clearly what has happened: we’ve hit the top bumper.
I don’t know if it’s better. But I am coming to agree with Bruce’s final notion. Given the design that we have, we probably do well to use that design rather than work against it.
So we’ll probably do that as we go forward.
Thanks, Bruce!
Tentative Plan2
So. We will create a top bumper and use it to terminate the player shot. That’s more consistent with our overall design and it will give Bruce encouragement to come up with another good idea.
We need to use the shot’s real bitmap, which is not yet imported, and its explosion, which I’m pretty sure is also not imported. We have an interesting question regarding the explosion. In Asteroids, we have some interesting explosions for the ship and saucer when they are destroyed, with lots of particles and animations. In Invaders, all that happens is a very simple display. I think the shot expiration explosion is just a single star-shaped bitmap that displays for a while. I’m not sure if it even flickers or not.
I see two ways we could handle it. One, of course, is a throw-down ShotExplosion, that we toss into the mix and that does its thing and times itself out. Or we could give the shot a state, exploding
and display a different bitmap for a while, and then remove the shot.
I think it’s pretty clear what Bruce would say. And, for consistency, he’s probably right. For convenience … I’m not so sure. We’ll probably go for consistency.
We also need to start dealing with the shot hitting invaders. I don’t think we’ll get to that this morning, but we’ll face the same issue: does the invader display her own explosion, or do we have a throw-down InvaderExplosion. We should do the explosions all the same way, including the as yet to come player explosion.
Hmm. With some luck … and a bit of skill if we can find any … maybe a single explosion object could handle all the cases. That is definitely an argument in favor of a separate object!
TopBumper
We’ll start with the top bumper idea. Let’s review Bumper:
class Bumper(InvadersFlyer):
def __init__(self, x, incoming_direction):
self.x = x
self.check = self.beyond_on_right if incoming_direction > 0 else self.beyond_on_left
self.incoming_direction = incoming_direction
def intersecting(self, rect: Rect):
return self.check(rect)
def beyond_on_left(self, rect):
return rect.bottomleft[0] <= self.x
def beyond_on_right(self, rect):
return rect.bottomright[0] >= self.x
class Invader:
def interact_with_bumper(self, bumper, invader_fleet):
if bumper.intersecting(self.rect):
invader_fleet.at_edge(bumper.incoming_direction)
Ideally, I’d want to use the same class for this top bumper as for the others. However, as defined, the bumper is only checking the x coordinates, and we really want to check y for our player shot.
I don’t see the advantage to making Bumper more complex. Let’s just create a new kind of bumper and use it. We do have some tests for Bumper so let’s see if we can test-drive a TopBumper.
class TopBumper(InvadersFlyer):
def __init__(self):
self.y = 40
def interact_with(self, other, fleets):
other.interact_with_topbumper(self, fleets)
This breaks things, because now my other objects need this new method. One of the PITA of this design is that we need to retrofit things … or to allow inheritance. I decide to allow inheritance. Remind me to talk about that.
With that in place I can continue my test.
def test_top_bumper(self):
fleets = Fleets()
fi = FI(fleets)
bumper = TopBumper()
shot = PlayerShot(u.CENTER)
fleets.append(bumper)
fleets.append(shot)
shot.interact_with_topbumper(bumper, fleets)
assert fi.player_shots
shot.position.y = bumper.y
shot.interact_with_topbumper(bumper, fleets)
assert not fi.player_shots
That test is a bit weak but it should pass when we’re done. Right now it doesn’t, because the shot is still in fleets at the end. This is our cue to implement.
In PlayerShot, upon interacting with the top bumper, we want to know if we’re hitting it, and if we are, we want to remove ourselves (and later, emit an explosion).
class PlayerShot(InvadersFlyer):
def interact_with_topbumper(self, top_bumper, fleets):
if top_bumper.intersecting(self.position):
fleets.remove(self)
class TopBumper(InvadersFlyer):
def __init__(self):
self.y = 40
def intersecting(self, point):
return point.y <= self.y
And our test passes. Let’s put a TopBumper in the coin and test in the game.
coin.py
def invaders(fleets):
fleets.clear()
fleets.append(Bumper(64, -1))
fleets.append(Bumper(960, +1))
fleets.append(TopBumper())
fleets.append(InvaderFleet())
fleets.append(InvaderPlayer())
It does work, I assure you. Let’s save the bandwidth and skip the video.
Commit: use TopBumper to detect PlayerShot off top.
We could do something more fancy, but why? All we care about is whether the object’s y coordinate is higher than the bumper. We could do some fancy rectangle thing and so on, but I don’t see why.
Often we are tempted to “generalize” things. Seeing that the side bumpers do support rectangles, we might say that we should, first, “generalize” TopBumper to support a rectangle and then, perhaps, “generalize” the bumper concept so that one class can support both side bumpers and top ones and bottom ones and probably middle ones, or even “generalize” to a small hierarchy of bumper types, or even “generalize” to three-dimensional bumpers in case we ever implement a three-dimensional game …
I’ve certainly done that sort of thing and surely will do so again, but my strong belief is that the above is a poor kind of generalization. It makes a simple thing more complicated. The best kind of generalization makes complicated things more simple. This time, at least, the problem is simple, the solution is simple, yet the solution does fit nicely within our overall design concept. We’ll leave it that way.
My thought above, about a single Explosion class handling all the different explosions? That might be the wrong kind of generalization. When it comes time to do the second explosion, I hope I am careful to keep things simple. One way that I often do that is to create a new thing, and then “discover” its overlap with another existing thing, and simplify by removing the duplication. That helps to ensure that I don’t generalize beyond my actual need.
Bitmaps
Before I can do the shot explosion, I need its bitmap and the bitmap for the shot itself. Excuse me a moment while I go set those up. OK, I’ve created some new bitmaps, just like the old bitmaps. I’ll spare you the details. We’ll use the shot now:
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(0, -8*4)
self.position = position + offset
self.velocity = Vector2(0, -4*4)
maker = BitmapMaker.instance()
self.bits = maker.player_shot
self.rect = self.bits.get_rect()
def draw(self, screen):
self.rect.center = self.position + Vector2(2, 0)
screen.blit(self.bits, self.rect)
Looks fine on screen, really the same as it did before. I moved it over 2 pixels to be better centered over the player. And that’s the wrong way to do it isn’t it? I should create it where I want it:
class PlayerShot(InvadersFlyer):
def __init__(self, position=u.CENTER):
offset = Vector2(2, -8*4)
...
Better. Looks good. What about the explosion? I’d like to see that before I do all the work, but no, let’s do the work.
def interact_with_topbumper(self, top_bumper, fleets):
if top_bumper.intersecting(self.position):
fleets.remove(self)
fleets.append(ShotExplosion(self.position))
That demands the rather trivial class:
class ShotExplosion(InvadersFlyer):
def __init__(self, position):
self.position = position
maker = BitmapMaker()
self.image = maker.player_shot_explosion
self.rect = self.image.get_rect()
def draw(self, screen):
self.rect.center = self.position
screen.blit(self.image, self.rect)
Of course I have to implement all the abstract methods. I set them all to pass
. Since I don’t remove the explosion yet, it poses for a photo at the top of the screen:
Now we have to time the explosion out. We’ll give it a second to live. Should we use a timer, or just count it down?
I vote for counting it down. Bruce? What do you think?
A bit of experimentation tells me that 1/8 of a second is about right, and I decide to color the explosion red just for fun.
class ShotExplosion(InvadersFlyer):
def __init__(self, position):
self.position = position
maker = BitmapMaker()
self.image = maker.player_shot_explosion
self.rect = self.image.get_rect()
self.image.fill("red",self.rect, special_flags=pygame.BLEND_MULT)
self.time = 0.125
def tick(self, delta_time, fleets):
self.time -= delta_time
if self.time < 0:
fleets.remove(self)
That looks nice. Let’s close with a nice video and a summary.
Commit: shot explodes in red at top of screen.
Summary
Swayed by Bruce’s remarks, we created a TopBumper, but kept it simple. Using that in PlayerShot gives us a nice separation between moving and exploding at the end of our travel. Still in the spirit of things, we added a new object, ShotExplosion, that just gets dropped in place by the PlayerShot, and exists for an eighth of a second, displaying a red explosion, and then vanishing forever.
Everything went nicely and while there are a few more lines of code, the responsibilities are nicely separated among the objects, given that you grok the basic design principle we’re following.
What about that inheritance trick?
Right. There was that. I defined interact_with_topbumper
not to be abstract, so that other objects may implement it, but are not required to. Thinking of the interact_with_
methods as events makes it seem reasonable not to implement them if you don’t care about them. But there is a drawback, and my decision to make them abstract methods was based on a real problem.
If we introduce a new object, like TopBumper, then we really need to consider, for every other object in the mix, how it will interact with that new object. Most of them may well ignore it, but we need to consider each one. In the past, in at least one and probably more than one case, I failed to consider how an object should interact, left out that interaction, and the game didn’t work correctly.
So I made the methods abstract, so that each object must implement each method. So when I made interact_with_topbumper
non-abstract, i.e. concrete in InvadersFlyer, I violated my own rule. But it is an irritating rule, because every time we add a new type of flyer, we add a new interact_with_
method, and therefore, if we make it abstract, we would have to edit every existing class to add that method. That comes with its own problems, of course. Here chez Ron, it’s only tedious. In a large application with lots of classes, it would require many small changes that could affect other developers adversely.
There is probably a more sophisticated approach that could be taken. I’ll just point out that if these were events, we would generally not expect to make everyone explicitly ignore a new event. The rule would be “if you care about this new event, subscribe to it”. It’s the same with the non-abstract interact_with_topbumper
. If you care about it, implement it.
That said, I should probably be consistent, and it might be wise to set up more comprehensive tests or reports about who cares about whom. But come on. I’m just some guy over here. I can’t do everything. I’m just making the best decisions I can given my limitations3.
Anyway, a nice step forward, with a top bumper and an explosion and a red color as well. A good morning to me and top of the day to you as well.
See you next time!
-
Why wise? Because on my worst day, my cleverness is not enough to understand the extra-clever code I wrote on my best day. I need all the help I can get on even the regular days. So I try to help myself by keeping things simple. Except when I don’t. ↩
-
I don’t know why I said “tentative”. All my plans are subject to revision. They never survive contact with the enemy.4 ↩
-
A man’s got to know his limitations. – Harold Francis Callahan, private communication. ↩
-
We have met the enemy, and he is us. – Pogo Possum, private communication ↩