Python 114
No, I mean REALLY look at it. Did you think I was finished with Thumper?
Yesterday afternoon, without writing an article, just to see what would happen, I converted Thumper to a Flyer subclass, so that Fleets no longer deal with it. I just put a Thumper into the initial load …
class Game:
def __init__(self, testing=False):
self.delta_time = 0
self.init_pygame_and_display(testing)
self.fleets = Fleets()
self.fleets.add_flyer(ScoreKeeper(testing))
self.fleets.add_flyer(WaveMaker())
self.fleets.add_flyer(SaucerMaker())
self.fleets.add_flyer(ShipMaker())
self.fleets.add_flyer(Thumper())
self.running = not testing
… and now we have thumping without Fleets needing to worry about it. Thumper now looks much like before, except with Flyer methods and checking built in. Recall that it only thumps when asteroids and ship are on the screen. Here is the relevant code:
class Thumper(Flyer):
def __init__(self, first_action=None, second_action=None):
...
self._saw_ship = False
self._saw_asteroids = False
self.reset()
def begin_interactions(self, fleets):
self._saw_ship = False
self._saw_asteroids = False
def interact_with_asteroid(self, asteroid, fleets):
self._saw_asteroids = True
def interact_with_ship(self, ship, fleets):
self._saw_ship = True
def tick(self, delta_time, fleet, fleets):
if self._saw_ship and self._saw_asteroids:
if self.it_is_time_to_beat(delta_time):
self.play_and_reset_beat()
if self.it_is_time_to_speed_up_beats(delta_time):
self.speed_up_beats()
else:
self.reset()
It works as advertised. This should be no surprise, it was a very straightforward conversion to Flyer, since the core object already existed.
So you’d think we’d be done, and frankly, it’s perfectly OK as it stands1. Still, there’s one way in which this object isn’t aligned with our overall design style: it does its own timing.2
We have a Timer object that we generally use for, well, for timing purposes. We should probably consider using Timer here in Thumper. Thumper actually has two timers, the one that decides to play a beat, and the one that decides to speed up the beats. The speed-up one ticks at a constant rate, while the beats one goes faster and faster at the direction of the speed-up one.
It seems to me that we can probably use Timer very easily for the speed-up timer, and all things being equal, we probably should. But the beats timer goes faster and faster. In Thumper the decision looks like this:
def it_is_time_to_beat(self, delta_time):
self._time_since_last_beat += delta_time
return self._time_since_last_beat >= self._time_between_beats
And the tricky bit is that this code adjusts _time_between_beaats
:
def speed_up_beats(self):
self._time_since_last_decrement = 0
self._time_between_beats = max(
self._time_between_beats - self._amount_to_shorten_beat_time,
self._shortest_time_between_beats)
Looking forward, it’ll be easy to swap in a Timer for speed_up_beats
. Let’s do it, i a spike, just to see how things will look.
We put this in init:
self._speed_timer = Timer(self._delay_before_shortening_beat_time, self.speed_up_beats)
And in tick
:
def tick(self, delta_time, fleet, fleets):
if self._saw_ship and self._saw_asteroids:
if self.it_is_time_to_beat(delta_time):
self.play_and_reset_beat()
self._speed_timer.tick(delta_time)
else:
self.reset()
That is easy enough and works just fine. But if we are going to use Timer in one place, we should probably use it in both cases, because otherwise the reader will be more confused than if we had used it in zero places.
I can see two possibilities for using Timer in the beat-timing logic. One would be to create the beats timer in __init__
and somehow adjust its delay. That would either require a horrible attack on the Timer from within Thumper, or a new feature on Timer to adjust the delay. Both of those ideas seem undesirable to me.
The alternative that I can think of would be “just” to replace the beats timer with a new one, every time the speed-up timer wants the speed to increase. That would be easy enough to do in speed_up_timer
. However, it would be creating and destroying Timer objects all the time. How often?
self._delay_before_shortening_beat_time = 127 / 60
Once every two and a bit seconds. I think we can afford it. Let’s put it in and see how it looks.
def reset(self):
self._beats_timer = Timer(self._longest_time_between_beats, self.play_and_reset_beat)
self._time_between_beats = self._longest_time_between_beats
def tick(self, delta_time, fleet, fleets):
if self._saw_ship and self._saw_asteroids:
self._beats_timer.tick(delta_time)
self._speed_timer.tick(delta_time)
else:
self.reset()
def speed_up_beats(self):
# self._time_since_last_decrement = 0
self._time_between_beats = max(
self._time_between_beats - self._amount_to_shorten_beat_time,
self._shortest_time_between_beats)
self._beats_timer = Timer(self._time_between_beats, self.play_and_reset_beat)
Not very different. The tick
looks nice. I could imagine keeping this code, except:
It sounds bad! When the speed-up timer swaps in a new Timer, it doesn’t happen on the beat, and so the beats do not come out at a constant increasing rhythm. Sometimes there is a delay before the next beat, sometimes it comes too soon.
We could, I believe, “make it work” by making the timer adjustable. I’ll try that, since this is a spike, by hammering the existing Timer.
def speed_up_beats(self):
# self._time_since_last_decrement = 0
self._time_between_beats = max(
self._time_between_beats - self._amount_to_shorten_beat_time,
self._shortest_time_between_beats)
self._beats_timer.delay = self._time_between_beats
Now the sound is just as before. As things stand, you’re not supposed to be putting new delay values into Timer, but it does work and we could bless it as legitimate, give Timer a new method for doing that etc etc.
But we’d be doing all this, with a bit of increased indirection and obscurity just to change this:
def tick(self, delta_time, fleet, fleets):
if self._saw_ship and self._saw_asteroids:
if self.it_is_time_to_beat(delta_time):
self.play_and_reset_beat()
if self.it_is_time_to_speed_up_beats(delta_time):
self.speed_up_beats()
else:
self.reset()
To this:
def tick(self, delta_time, fleet, fleets):
if self._saw_ship and self._saw_asteroids:
self._beats_timer.tick(delta_time)
self._speed_timer.tick(delta_time)
else:
self.reset()
We could do this. It would work. But while it might simplify that one method, it would be overall a bit more obscure, and would require either a change to Timer that no one else needs, or an invasion of Timer’s members from outside, which, really, just isn’t done. We should not do this3.
Roll back.
Summary
We might gild the lily around here, but we will not embed small diamonds around its edges. So was this a waste of time?
I think not, because it really just took a couple of lines of code to get a concrete comparison of what might have been a better way. I might have been smart enough to see in my imagination that it wouldn’t be better, but in fact I’m not that smart: my imagined solutions are usually much better than the real ones. I might have been tired enough to have said “oh probably not worth it” and moved on. Either way, I would have made the decision with less real information than I now have.
Often, if it’s a matter of a few minutes, trying something in a spike is better than just thinking, and better than just not bothering.
At least that’s how I see it.
See you next time!