Space Invaders 33
I want to figure out why this program makes me feel bad. Then do something about it.
I’ve mentioned before, most recently in Space Invaders 32, that I feel like this program is beating me up. I’d like to figure out why I don’t feel good about it, what my difficulties are, and what to do about it. There’s no sense in just complaining about things like this, I’m the only person in here, so I’m the only person who can take effective action.
I do have a quick change to report, and at least one other small one to do as well. The quick change is that there were a few lines where readImage
was reading from the dropbox folder, and if anyone had tried to load the program, that wouldn’t have worked. The fact that I received no bug reports about that tells me something about my readership and user base.
I changed those references, which were in Army
, and committed. I found that bug when I decided to play with the program on my other iPad, and it reported that error. My purpose on the other iPad was to try an idea I lifted from Dave1707 on the Codea forum, who whipped up a cute little game where you fly a ship around in a soft of first person shooter view, trying to run over balls that are scattered around. Every time you get a ball, a random wall appears somewhere, so it gets harder and harder to get all the balls. It’s kind of a fun game: if you have Codea, you might enjoy playing it and reading the code.
In Dave’s game, you guide the ship by titling the iPad left or right, and in later versions, forward and back as well. I find moving the Space Invaders gunner side to side to be awkward with my current hot screen areas, so I thought I’d try the tilting idea.
It turns out to be less than one line of code to do it, and I rather like the result. So, time permitting etc etc, I’ll pop that in today.
But today’s big task is to figure out why I’m not entirely happy with how things are going, and how to fix it.
Looking back, progress on the game has been pretty steady, and every time I’ve set out to add a feature, it has gone in pretty well. In so doing, I have often found that the code seems poorly organized, not as tight and modular as I like. I don’t usually feel slowed down much by that: most everything added just requires tweaks in a few spots that are easy to find.
I think that a time or two I’ve missed a tweak but quickly saw the error on screen and was able to fix it quickly. I recall no time when the actual game code left me confused or unable to do something. (A review of the articles might remind me of one but get real, there are 32 of them.)
Bottom line, the game programming has gone quickly, with features going in pretty readily.
I’ve had issues with tests. In one case, the system’s new structure made a test obsolete and since it would have been hard to fix the test, and the code worked, I deleted the test. Then, more recently, I discovered an intermittent test failure, perhaps one in 20 runs or more, that really boggled me. I couldn’t figure out what that was (I just now got an idea, however) so I deleted that test.
The idea is: there is a low-probability event inside invader motion, where every now and then they drop a bomb. What if that was happening during the test, and what if the test setup wasn’t prepared to handle it, and what if the testing framework didn’t capture the location of the error correctly? That’s a lot of what if. If the test were still in the system, I might explore that. It isn’t, and I probably won’t.
Now, visual games are hard to test, or at least hard for me to test, but when my testing game is on, they give me so much confidence in the code that I miss them. So part of the problem here is that when tests fail and have to be removed, they chip away at my confidence. That may be good, because with such a thin net of tests as I have, confidence in them isn’t well-placed.
The tests in question were not micro. They involved the testing of complex behavior of multiple objects. Such tests may be needed, and if they are, we should write them, but as the design evolves, they tend to be brittle. Some of my colleagues rarely automate such tests at all, using manual testing, visual inspection, and for all I know magic to figure out that the complex behavior works. I often skip them on the basis of inspection and manual testing myself, but I don’t really think it’s a good idea.
So … much of my bad feeling about things may be coming from my problems with the tests. I think the important thing to note is this:
These are bad feelings about me, not about the program.
The program is coming along well. My objective assessment, to the degree that I can have one, is that features have gone in quickly, that there are few if any defects in game play, and that progress has been steady.
Imagine that you were programming something, in a language you were supposed to know, and somehow, on that day, virtually every statement you typed in wouldn’t even compile, and it seemed to take ages to figure out what was wrong, so that you began to look around for the other 999,999 monkeys to see how their random programming was going.
You might feel bad about yourself. “What’s wrong with me today? Why can’t I get two statements in a row to work? Am I not as good as I think I am?”
That’s kind of how I feel. I’m supposed to be better at this than I have been, especially with my tests …
And yet, the program is coming along just fine.
Is there nothing to learn then?
What’s to learn? Well, even if it’s just me and my confidence in me, something needs to be done. But there’s still a way to think about this that doesn’t require me to see the chaplain or my local mental health professional.
I lost confidence because two of my most complex tests had to be deleted. Restate that: because I decided to delete two tests rather than make them work.
If microtests are hard to write, the message from the computer to us is that the code isn’t well-configured to be testable, and that is almost always a sign that it isn’t well-configured at all. There is usually a design problem to be found when tests are hard to write.
So my discomfort over being even less smart than I think I might be, should be transformed into “there is something wrong with this design, and we should do something about it”.
Add to that that I’ve been grumbling about the overall code for weeks, and it adds up to time to clean up some stuff, and perhaps even to write some new tests, if there are things we’re not comfortable with.
I’m glad we had this little talk. Now, after a Chai run, let’s see what to do about it.
Game Runner
Ive been thinking that the object called Army
is doing a lot more than operating the army. It manages update timing. It keeps track of the bombs and the gunner’s Missile. It draws the invaders and the bombs (but not the missile, which is odd). It checks collisions between bombs and missiles, and between invaders and missiles. It tells bombs when to update (which is when they check for whether to explode). It knows, on behalf of the invaders, how much they add to the score when they’re destroyed.
It probably does other things. Way more than something called Army
ought to do.
At the same time, we have lots of stuff going on in the Main tab, including creating the Army instance, creating the shields, setting up the Gunner, initializing the Missile, zeroing out the score, and creating the line that goes across the bottom of the screen.
At draw time, it explicitly draws the test results, the army, the gunner, the missile, the shields, and the status. It updates the army.
There’s a lot going on, and there’s very weird division of labor here between Main and Army tabs.
I think this code is asking for something, and I believe that it may be asking for a GameRunner
object. This is not something I would do lightly. A game runner is a sort of “god” object, and, according to my first- and second-hand experience, god objects are generally to be avoided.
In this game, we do have a need for a bit of a top-down structure, in that there are many things that need to be drawn, we have a specialized update process for our army, designed to make it move in a visibly step-wise fashion. We have various objects that interact with each other. And we have Codea.
Codea works by triggering draw
events every 1/120th or 1/60th of a second. When that event occurs, it is time for your whole program to update whatever it needs to, and draw a new frame. Everything needs to happen under control of draw
, which means that there is at least some kind of fundamental top down connection from that function down to all the things that need updating.
Compare to “Legacy”
If we look at the original Space Invaders game, it’s not dissimilar. It gets interrupts every 1/60th of a second, I believe. It has two or three screen cursor states (top, middle, bottom of screen) and it does all its work during the interval between those interrupts. The original game does some odd things, like only changing the top of the screen while the bottom is being drawn, only updating invader bombs every third cycle, and so on.
The original game is a big blob of code. It certainly includes functions, but it basically runs from top to bottom, checking state of an invader or bomb or whatever it’s looking at, branching to this or that code. It’s pretty monolithic. I’m finding it hard to understand. Without the excellent comments in the version I found, I’d have no idea how it works.
Basically, the legacy program is a big game runner loop with everything hung directly off of it. If something needs to be done before something else, it had better be in front of it. If something is in front of something else, it may be because it needs to be, or it may just be that everything has to be somewhere.
With a game runner god object, accidental behavior and intentional behavior get tangled up together, and when we need to change the overall game flow, we have to change the code. That can be bad.
But our game is small, and the flow is nearly settled at the individual player level. (We may need to deal with attract mode and two players. In principle we must: in practice we might skip it.) We could argue that what we have is small and the god object will be easy enough to deal with. And we’d be right.
We could also argue that the god object will cause us trouble, and that even in the small, it’s quite likely not the ideal way to go. And we’d be right.
We could further observe that we area part way between a possibly inferior god object design, and some kind of strange ad hoc non-design where we just plunked things down where they seemed to go. And we’d definitely be right.
So what to do?
I’m glad we had this little chat, too. When we started, I was going to turn Army
into GameRunner
and move everything into it. Now I’m not so sure.
In a way, I’d like to see what comes out if I do build a god object game runner. I’m quite sure that I’d re-invent the Army class, because of the special drawing that the invaders require, and because I know of some upcoming needs that require thinking of them as a group, such as not dropping bombs through one’s underlings and such. I said underLings, not underTHings.
Also in a way, I’d like to see whether there’s a way to make the objects more autonomous, perhaps even going so far as to “register” for draw and update events, making those activities a bit less explicitly procedural.
Why not both?
I’m going to argue here, and convince myself, that the notion of a single god object game runner is almost certainly a bad idea, and therefore I’m going to do it. I’m going to do it to find out how bad it is, and to discover what can be done about it if it’s troublesome.
However … my original thought was to rename Army
to GameRunner
and move more stuff into it. I think that’s clearly not ideal. Let’s go with this plan instead:
Implement a new GameRunner object to manage all the other objects and their sequencing. Move behavior from Main, Army, and elsewhere as needed.
OK, we have a plan. Let’s do it. But what about gravity, you promised us gravity.
Gravity, then GameRunner
OK, gravity.
The Codea global variable Gravity
returns a vec3
whose x, y, z coordinates are “the amount of gravity” in each of those directions. X is screen-tilt left to right, y is front to back, and z I have no idea. It seems to me that if you have the other two, z is determined.
Anyway, we just want left-to-right tilt, so that when you tilt the screen side to side, the gunner slides left to right. I tested this on my other iPad and here’s what seemed to work. Gunner doesn’t have a separate update call. (That’s almost certainly bad.) It has this, in draw
:
function drawGunner()
pushMatrix()
pushStyle()
tint(0,255,0)
if Gunner.alive then
sprite(asset.play,Gunner.pos.x,Gunner.pos.y)
else
if Gunner.count > 210 then
local c = Gunner.count//8
if c%2 == 0 then
sprite(GunnerEx1, Gunner.pos.x, Gunner.pos.y)
else
sprite(GunnerEx2, Gunner.pos.x, Gunner.pos.y)
end
end
Gunner.count = Gunner.count - 1
if Gunner.count <= 0 then
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
Gunner.alive = true
end
end
end
end
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
Down at the very bottom of draw, we update gunner. That’s going to be machine-speed dependent, which we don’t want, but we’ll handle that on bug reports. (Sorry, I’m in a hurry. See me making a mistake here? Yes. Yes, you do.)
The gravity goes in like this:
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
That’s all there is to it. We adjust the gunner position by 5 times the x component of gravity, in the x direction. We never update the gunner’s y.
Why 5? Well, “it worked on my machine”. A reasonable amount of tilt seemed to range between -0.2 and 02 in x. The GunMove from touch is 1. So 5*0.2 is 1.
A quick test … tells me that it’s too sensitive on my faster iPad. See how quickly our mistakes catch us up? I also notice another issue which is that the position is updating when the gunner is respawning, and that shouldn’t really happen. We should condition our move on Gunner.alive:
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
end
I was going to skip doing a separate gunner update method but OK, I’m here now. So:
function updateGunner()
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
end
end
I extracted that from the draw
where can we put the call to it? Right now, it’s in Army:update
and doUpdate
that we handle the update timing, so we can fix the timing bug if we call from doUpdate
, which looks like this:
function Army:doUpdate()
if Missile.v > 0 then Missile.pos = Missile.pos + vec2(0,4) end
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
Does it matter where we call the function? Well, Bomb:update
includes the collision checking for killing the gunner. So let’s update the gunner first, to give him one more tick of a chance to get out of the way.
function Army:doUpdate()
updateGunner()
if Missile.v > 0 then Missile.pos = Missile.pos + vec2(0,4) end
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
That looks pretty good, the gunner doesn’t move quite as quickly, and I can tell that my keyboard tray isn’t quite level. We may want a run-time toggle for gravity, but not today.
Commit: gunner gravity control, update under Army.
Game Runner
OK, game runner. I’m going to make a new class. My long-term design criteria include:
- All object initialization lives under GameRunner
- All top-level draw events stem from GameRunner
- GameRunner manages update cycle and all top-level updates.
- Whether GameRunner manages collisions remains to be seen.
By “top level” I mean that an object like Army could legitimately send draw and update events to objects under its control.
I’m not clear in my mind on whether collisions should be managed at the GameRunner level or not. I’m imagining another way to do that, which might have to do with a general collision handler that knows two collections of objects that need to be checked for collision. As we encounter code during this refactoring, we’ll see opportunities to decide where to put it and how to make it better.
This is a refactoring. There is, in principle, no new behavior here from a systems viewpoint. The game will look the same: its implementation will change.
In practice, we might do new features in the middle of this refactoring, but we’ll do the refactoring itself in small steps, each one committed, so that we can interleave functional changes at will.
That’s how you do “large refactorings”. You turn them into a bunch of small refactorings.
Step one, GameRunner tab and class:
-- GameRunner
-- RJ 20200831
GameRunner = class()
function GameRunner:init()
end
function GameRunner:draw()
end
function GameRunner:touched(touch)
end
Perfect. Commit: GameRunner class.
Silly, but I have a bad record of not committing often enough, so I’m trying to get in the habit.
Clearly the Main tab needs to instantiate the GameRunner, and I reckon we want it to be a global. And Main should call its draw
and touched
events as well. The setup
is easy:
function setup()
runTests()
Runner = GameRunner()
createShields()
createBombTypes()
TheArmy = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
invaderNumber = 1
Lives = 3
Score = 0
Line = image(208,1)
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
end
The draw
event is messy. The touched
event is currently in Gunner, since only Gunner cares about it. We’ll leave that for now. I guess there’s nothing for it but to dig into Main’s draw
:
function draw()
pushMatrix()
pushStyle()
noSmooth()
rectMode(CORNER)
spriteMode(CORNER)
background(40, 40, 50)
showTests()
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate(WIDTH/8-112,0)
fill(255)
drawGrid()
TheArmy:draw()
drawGunner()
drawMissile()
drawShields()
drawStatus()
popStyle()
popMatrix()
TheArmy:update()
end
There’s a lot going on there. Much of it is initializing the general graphics area, and for now, anything that the game runner does will want to rely on that. So I’ll put the call to GameRunner:draw right after
drawGrid`, which is a sort of debug call anyway.
function draw()
...
drawGrid()
Runner:draw()
TheArmy:draw()
...
end
This runs. The tests run and so does the game. No surprise, we’re still not doing anything interesting. Commit: GameRunner initialized and drawn.
Let’s make it do something. I’ll move the initialization of the army in, and then the drawing.
function GameRunner:init()
TheArmy = Army()
end
function GameRunner:draw()
TheArmy:draw()
end
These are removed from Main, of course. Commit: GameRunner inits and draws Army.
I noticed that the Gunner can drift clear off the screen. I should make a card for that.
Or I should fix it. Let’s do that.
function updateGunner()
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
end
end
We need to clamp Gunner.pos.x between zero and 224. Well, actually, between 11 or 12 pixels inside those values, since the Gunner has width. I’m not entirely happy that we are doing this vector arithmetic when we only change the x value. Anyway …
function updateGunner()
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
Gunner.pos.x = math.max(math.min(Gunner.pos.x,212),12)
end
end
That sort of works, but I note that our line across the bottom seems to go all the way to the left of where the gunner can go, but not all the way to the right.
What’s up with that?
The line is 208 pixels wide for some reason:
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
And it is drawn starting at x = 8:
function drawStatus()
pushStyle()
tint(0,255,0)
sprite(Line,8,16)
...
So that makes it go from 8 to 216. 224-16 is 8. So it should be centered. What silly arithmetic error have I made now? Ah. I forgot that the Gunner has a bottom-left origin, not a center origin. Not like that hasn’t bitten me every day for a couple of months. Fix the clamp:
function updateGunner()
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(5*Gravity.x,0)
Gunner.pos.x = math.max(math.min(Gunner.pos.x,208),0)
end
end
That does the trick. The Gunner overhangs the ends of the green line equally on each end.
Commit: fixed defect where gunner ran off screen.
Do you see what just happened? We fixed a serious bug–OK, maybe not terribly serious–right in the middle of a “large refactoring”, with no trouble at all. Why? Because we aren’t doing a large refactoring at all, we’re doing a lot of little refactorings that will add up to a big design improvement.
As my friend and colleague GeePaw Hill would put it, we’re harvesting the value of small changes. In this case, our harvest included the ability to fix a defect right darn now and release the code, while not disrupting our progress on improving the code.
It’s about time to wander over to Wendy’s to pick up a chicken sandwich. Let’s do one more thing in our refactoring: let’s move the update clocking from Army into GameRunner.
We’ll need to move Army:update
to GameRunner
, and rename Army:doUpdate
to update
.
This breaks everything. The tests don’t even run at all. I may have to back this out but I’ll take a quick look first.
GameRunner:27: attempt to perform arithmetic on a nil value (field 'updateDelay')
stack traceback:
GameRunner:27: in method 'resetTimeToUpdate'
GameRunner:8: in field 'init'
... false
end
Ah that’ll be in Army. Move to GameRunner.
function GameRunner:init()
self.updateDelay = 1/65 -- less than 1/60th by a bit
TheArmy = Army()
self:resetTimeToUpdate()
end
The tests all pass now. Whew. Game crashes:
GameRunner:19: attempt to call a nil value (method 'doUpdate')
stack traceback:
GameRunner:19: in method 'update'
Main:41: in function 'draw'
Yeah, well, we do need to implement that, and make it update the army:
function GameRunner:update()
if self:itIsTimeToUpdate() then
self:resetTimeToUpdate()
self:doUpdate()
end
end
function GameRunner:doUpdate()
TheArmy:update()
end
Tests green, game works fine. Commit: move main update decision and army update to GameRunner.
Seven commits this morning. Time for a lunch run. I’ll sum up upon my return.
Summing Up
Well, I’m feeling much better about things. It’s not so much that the world is a better place today, but I have a plan in place, and have made some progress on it, and it seems clear that the code is already a bit better. There’s a new place for a kind of functionality that was baked into two places, neither of which was quite right.
You could argue for making the Main tab the game runner, but with our tests being blended in there, and the fact that the Main isn’t an object, it seems to me that making a GameRunner object is a better choice.
I think the big lesson is this: listen to your mind, your feelings, and your body. They will tell you how things are going, and if they say things are not going well, trust them, and come up with an approach to improve the situation. We probably can’t get rid of all pressure during development–though I believe we probably should–but we should definitely not be feeling a sense of hopelessness or impending doom. Those are signs that something needs to change.
Often those changes are changes we can make in our approach to programming, and those are the ones I mostly focus on in these articles. But sometimes we need to change how we communicate with our team, or our management, about the programming. And sometimes, we need to change where we work. I think that by delivering working software on a regular basis, and keeping it correct enough and clean enough to keep progressing week in and week out, we have the best chance of having things work out where we are.
And that’s generally the best situation.
Anyway, I’m happier today and expect to be happier tomorrow, if the crick don’t rise.
See you then!