Some Reflection
Python Asteroids+Invaders on GitHub
I look back at what one might have learned along the way here. Joy: highly recommended. Find yours.
Why?
I freely grant that my primary purpose in doing Space Invaders was to entertain myself and to have some live code to manipulate and write about. As we’ll talk about below, I think that any and all code that we write tends to teach us the same key lessons.
A secondary purpose was to show that the “decentralized” character of my Asteroids game allowed it to support other games without changes to the basic objects. Instead, one just creates new, independent, interacting objects like InvaderPlayer and InvaderSaucer, puts them in the mix instead of Asteroids objects, and as if by magic, a new game appears.
What is it?
That this worked is pretty clear: there are two games running in the same repo. So, QED, I guess. The basic flow of either game is the same, and is managed by the same code, primarily the Fleets object, which sends messages update
, begin_interactions
, interact_with
, end_interactions
, tick
, and draw
. Each message is sent to all the known objects in the Fleets collection. Everyone sees update
, then everyone sees begin_interactions
, and so on.
Generally speaking, objects move themselves during update
, initialize to collect any information they need on begin
, and compute and manage collisions and other interactions during interact
. At end_interactions
, an object knows that it has seen every other object in the mix, and can make decisions based on that information. An object might observe that it had not seen a Player and might set up to create a new one, for example.
The tick
method is used occasionally to do timing-related things: the InvaderExplosion removes itself during tick
if the excitement has gone on long enough, and so on.
And draw
, of course, is the moment when a visible object draws itself on the screen.
The trick of the trade with this design is in figuring out how to get global game behavior from independent interacting objects. A simple example is scoring.
An invader knows the score the player will get for shooting the invader. In a simpler era, when an invader got hit, the central program marked the invader dead, started an explosion, and added the value to a score variable somewhere. Later in the main loop, the program painted the score on the screen. In our scheme, an invader that gets hit removes itself from the mix, adds an explosion to the mix, and adds a Score object to the mix, containing the invader’s score.
Lurking in the mix is a ScoreKeeper. If it receives an interact_with_score
message, it adds the score to its accumulated total. When draw
comes around, it draws the current total. Meanwhile, if a Score object sees the ScoreKeeper, it removes itself, so that it will only be counted once. Interactions are done on a pristine copy of the mix before interactions start, so that any additions and removals have no effect on the current cycle of interactions.
Is this “the right way”?
Obviously this is a more complex interaction than having a central control program that just adds scores to totals and draws them. That said, if we had Asteroids written in that old compact style and they told us we had to make Invaders run in the same code, I think we’d have been looking at a pretty big mess.
Am I saying that this program is the right way to build games like these? Not at all: in my view there is rarely if ever one right way. This program demonstrates one interesting way of writing games, by providing a central framework that emits certain events, into which you can insert individual objects to create your game.
What can we learn?
Does all this tell us anything about an overall design for other kinds of programs? Perhaps. I’d have to think about it. Certainly there are some interesting ideas about how objects might collaborate to get something done.
But inside the code, it seems to me, the same lessons arise again and again, without much regard to the kind of program we’re writing. We decide how to partition behavior, how to allocate behavior to different objects, how objects interact, how to maintain and update state, and so on.
As we evolve the program, we see the same signals over and over. We see duplication of the same or similar code. We see one object asking questions of another rather than telling it what to do. We see complicated nests of conditionals or loops. And the moves we can make are often the same as well. We remove duplicated behavior by creating a common function, or a common method, or even an object that holds that behavior. We reduce the complexity of one method by splitting it into two or more others … and we reduce the complexity of an object by splitting it into two or more others.
Almost everything I’ve done in this program is something other programmers—and I— have done in many other programs with all kinds of purposes. The fundamental moves of the programming game are much the same wherever we go and what we do.
And the mistakes. I make the same mistakes, over and over, again and again. I misspell things, I misremember how something works. I forget some important detail in setting something up. I leave out punctuation, or put in too much. I write code that digs too deeply into another object, or not deeply enough. I call the wrong method or pass in a perfectly good parameter except that it’s not the right one.
And too often, so much too often, I take steps that are too large, crawling further and further out on a limb of code that doesn’t work. Sometimes I’m lucky and it works. Even then, the code is often rather scattered and ugly. Often, it doesn’t work at first and after long debugging, I force it to work. Again, the resulting code is less than pristine.
And too often, so much too often, I don’t have tests. Maybe I just “know” what the code should be and want to put it in. Or I don’t quite know what it should be, so I can’t see how to test it until I write it. Sometimes I just want to experiment, but then decide that the experimental code is good enough, and rather than roll back and do it over, I decide to keep it and clean it up.
And quite often … I get away with it. I make the thing work. Sometimes I even put in a few tests afterward, or at least test it carefully in the game. Except when I don’t.
Parts of what you see in these articles shows the programmer that I’d like to be, clear-thinking, excellent practices, good ideas, rare mistakes caught immediately by carefully-crafted tests, readable, well-factored code, rapid progress, good results.
More often, you’ll see some of those things happening but not all the time and not all of them at once: the programmer that I am rather than the one I’d like to be.
And all too often, you’ll see me producing code that is mediocre or worse. I think I usually come back later to improve it, because that’s part of the job and part of what keeps me from bogging down entirely. But when we review this program, which we’ll surely do in the future, I’m sure we’ll find some things that, well, let’s just say “would benefit from a bit of improvement”.
And that, in my view, is the reality of programming. On our best days, we’re nearly good. In our very best moments, we might actually be quite good. The trick is to develop habits that tend to keep us near to our best moments, and to work, daily, to keep those habits alive, because day in and day out, our performance varies. We cannot operate at our peak all the time.
I wish that I would write tests every single time before I write code. I wish that I would take such small steps that my every mistake was instantly apparent. I also wish that I would win the lottery and that pigs could fly.
None of those wishes seems to be coming true all by itself. The programming ones, I can help along by staying calm, remembering the things that work and reinforcing them by using those good ideas and discovering, again and again, that I like what happens when I write tests for exceedingly small tests and make them run. The lottery and pigs, well, I’m stumped there.
Small tests and small tests is what I do, and what I try to do. No matter what kind of programming I’m doing, that’s what works for me. What will work for you? I could guess, but perhaps it’s more important that you try new and old ideas, shuffle them around, and see what seems to fit.
When I take a big leap and make it work, what I generally feel is relief, maybe a reduction of the pain in my neck that has built up from stress.
But every time I write a little test, then make it work with a little code, I get just a little kick of joy. A tiny “whee!!”, or “yippee!!” .
Joy. Highly recommended.