Space Invaders 27
Let’s do some damage!
Here we are on Monday. I’ve decided to call the previous article a wrap and publish it, then do this new one for Monday. The previous one is full of fail and today, today’s going to be my day.
I’m pretty confident in the new Shield;hitBits
function, so I think this morning I’ll plug it into the program, and then work on displaying damage in the Shields. That will also let bombs drill down slowly through the shield, although that may take a while to see during gameplay.
We also need to use this same kind of thing for the bombs hitting the gunner, and if we’re going to have them damage the line at the bottom, there too.
The hitBits
function looks like this:
function Shield:hitBits(pos,w,h)
for x = pos.x,pos.x+w-1 do
for y = pos.y,pos.y+h-1 do
local has = self:hasBit(x,y)
if has then return true end
end
end
return false
end
Our present code for damaging shields is in the Bomb code, which troubles me, and it looks like this:
function Bomb:update(army, increment)
...
if self:killedShield() then
army:deleteBomb(self)
return
end
...
end
function Bomb:killedShield()
for i,shield in ipairs(Shields) do
local hit = self:damageShield(shield)
if hit then return true end
end
return false
end
function Bomb:damageShield(shield)
local shieldPos = shield.pos
local hitPos = rectanglesIntersectAt(self.pos,3,4, shieldPos,22,16)
if hitPos == nil then return false end
shield:applyDamage(hitPos)
return true
end
There in damageShield
is where we need to add our call to hitBits
, before we call applyDamage
.
function Bomb:damageShield(shield)
local shieldPos = shield.pos
local hitPos = rectanglesIntersectAt(self.pos,3,4, shieldPos,22,16)
if hitPos == nil then return false end
if not shield:hitBits(self.pos,3,4) then return false end
shield:applyDamage(hitPos)
return true
end
This nearly worked. I could see damage working the edges of the Shield, where it bends downward. But the bombs never dig in. That’s because, despite the damage looking black, it’s really red, because of the tinting. We’ll change that:
function Shield:applyDamage(hitPos)
local relativePos = hitPos - self.pos
for x = -1,1 do
for y = -1,1 do
self.img:set(relativePos.x + x, relativePos.y + y, 0,0,0)
end
end
end
The set
call now sets color to 0,0,0, which is black, and the value we need. Now with the bomb density turned up, we should get a good result.
Well, yes and no. We’re definitely damaging deeper into the shields, but the damage patterns we’re making leave pixels around that get hit by the next bomb, but do not get destroyed. That’s not too surprising, since we’re just doing a few pixels right around the hit position, basically a 3x3 spot around the lower left corner.
We should clearly damage anything in the actual range of the bomb, and it seems to me that the bomb should dig downward as well. Also, we shouldn’t allow the damage to be offset to the corner, it should be equal on both sides.
I’ll try this:
function Shield:applyDamage(hitPos)
local relativePos = hitPos - self.pos
for x = -1,3 do -- one pixel each side of bomb
for y = -3,3 do
self.img:set(relativePos.x + x, relativePos.y + y, 0,0,0,0)
end
end
end
That took just a little tuning. I tried y starting at -4 and decided that made the bombs dig a bit too well. And I added a zero to the set, to set transparency, because our background isn’t pure black, and the damaged shields were showing black rectangles once damaged.
My virtual helper on the Codea forum, Dave1707, did an interesting demo of more random damage, but his shields had lots more bits in them. I’m emulating the 224x256 display of the original game, and he was using the full power of the iPad screen.
I think this is good enough for now, but we could think about a bit more randomness in the explosion if it seems valuable. Right now, I vote no.
I think we can set the bomb dropping percentage back to normal, and commit: bombs check shield bits and damage appropriately.
Let’s reflect a bit.
Reflecting a Bit
(No pun intended.)
After enough fumbling to make me wish I didn’t tell the truth in these articles, the current implementation of bomb-shield collision and damage isn’t bad.
It’s also not great. I think we’ll review it tomorrow. I’m of a mind to do some more work this morning, but this article is already rather long, so I think I’ll wrap this one up after these remarks.
I still think I got too enamored of making the code “right”, and while I believe I was right to observe that the collision responsibility isn’t all in the right place, it was too soon to deal with it. Almost always, if I just look at little bits of code and move them where they seem to want to go, things get better. This time, they didn’t.
I have some ideas on why my usual small changes approach didn’t work well.
The code wasn’t really working
First, the code I was trying to improve wasn’t really fully working. It was only about 80 percent “there”, and refactoring code that’s not baked is risky.
My brain wasn’t ready
Second, while it’s true that I am inclined to simply look at code and move it to a better place, I usually have in mind at least a half-baked notion of where things belong. In this case, I didn’t, and still don’t.
Why don’t we know where this code belongs now, you may ask. We do have decent rectangle checking code and bitmap checking code, that much is true. But we’ll have almost exactly the same code for bombs vs gunner, and we have a similar but different problem for aliens eating shields when they start moving across at shield level.
I plan to wait for those stories to come up before I do very much to this code, having learned the lesson that I’m not ready. But I’m not going to run scared: we will look to see how to make this code right for the current situation.
The zero-one problem
The image is one-based, and positions are zero-based. That makes for a lot of fiddly plus-or-minus-one code. That’s easy to get wrong, and things don’t work right but it’s hard to see quite what’s going on.
I am sore tempted to resolve this problem somehow, but that would require me to build some kind of infrastructure thing for bitmaps and/or graphics, and there’s no legitimate reason to do that.
I had very few tests
Let’s get real here. If you’ve got rectangles containing random bits, and some of them are flying around, and you need to know whether they are intersecting, this is not code you want to be checking by eyeball. You need tests.
It would be sufficient to write tests on the existing code, though it’s particularly tedious when we’re pretty confident in our towering ability to code. I even resist it when I know the code’s not right. I’m a programmer, Jim, not a tester.
Except that TDD isn’t testing, it’s design and it’s programming, and it turned out to be really helpful when I finally got around to doing it. The result is a very decent suite of checks on the correctness of the hitBits
function, and except for the fact that it lives on Shield
, it looks to be easily generalized.
Starting over helps
This is a lesson I have learned well, and have difficulty applying even so. When I seem to be stuck on something that should be straightforward–or something tricky–it almost always pays off to revert the code, take a break, and then come back at it fresh.
I think that what happens is that we’ve begun to build up a lot of intuition about the problem and the solution, but that the code we have doesn’t reflect that learning. We’re not even aware of that learning yet, because the details of the code before us swamp out the bigger picture. Stepping away lets things come into balance.
But it’s so hard to give up, isn’t it? We’ve beaten our heads through other problems, and we can bash through this one as well.
But starting over helps.
And now for something …
I was originally just continuing the Friday article but decided instead to spit it at this morning and ship it. So that’s done, and it’s still only 0920, and I’ve got my chai, Margaritaville on the HomePod, half a charge on the iPad, and a granola bar, so let’s see what’s next.
From a game viewpoint, I think what should be next would be that bombs should be able to kill the gunner.
Have you noticed the inconsistent terms for the player thing? Is it bothering you? It’s bothering me, but my brain hasn’t solidly taken to a term. In the code it’s Gunner
, but I think of it as the player sometimes. Oh well, that’s not the stupidest thing I ever did.1 We’ll carry on.
So we have half-decent detection on bomb-gunner collision, and we turn the gunner red as an indication of the hit. We’ll need to improve that to deal with the fact that the gunner isn’t really a rectangle, but that can wait for the basic functionality.
We’ve drawn a dummy count of lives and extra gunners at the bottom of the screen:
That’s just constant info right now:
function drawStatus()
pushStyle()
stroke(0,255,0)
strokeWidth(1)
line(8,16,216,16)
textMode(CORNER)
fontSize(10)
text("3", 24, 4)
text("CREDIT 00", 144, 4)
tint(0,255,0)
sprite(asset.play,40,4)
sprite(asset.play,56,4)
popStyle()
end
Suppose we had a fairly global variable called Lives, and we used it in the code above, like this:
function drawStatus()
pushStyle()
stroke(0,255,0)
strokeWidth(1)
line(8,16,216,16)
textMode(CORNER)
fontSize(10)
text(tostring(Lives), 24, 4)
text("CREDIT 00", 144, 4)
tint(0,255,0)
local addr = 40
for i = 0,Lives-2 do
sprite(asset.play,40+16*i,4)
end
popStyle()
end
That’s a bit of a hack, but not terrible. Can’t be too bad, it works. Now where do we turn the gunner red?
function Bomb:update(army, increment)
...
if self:killsGunner() then
army:deleteBomb(self)
Gunner:explode()
return
end
...
end
function explode()
Gunner.alive = false
Gunner.count = 120
end
Whoa! That’s interesting. Gunner isn’t a class, and so Gunner:explode
just calls the global explode
, passing it the Gunner, which it ignores. Weird. We were on a path to make the Gunner into an actual class but never got there.
Should we make it right, right now, or push forward to add our feature? Making the code right is almost always better, so let’s make the mistake of adding functionality to this not-so-very-right code. We’ll see how much trouble we get into, and how hard it is to make things right if and when we decide to.
There are two phases to a gunner explosion, and the bitmaps are available. Let’s first change the code to use those rather than just turn red.
function setupGunner()
Gunner = {pos=vec2(104,32),alive=true,count=0,explode=explode}
GunMove = vec2(0,0)
GunnerEx1 = readImage(asset.playx1)
GunnerEx2 = readImage(asset.playx2)
end
How long does the invader explosion last? We can use that as a guideline for the gunner. The invader explosion lasts for 15 ticks. We’ll follow that lead. We may have to make sure they’re the same kind of ticks, remember that we try to update every 1/60 second but my fast iPad sometimes updates every 1/120.
Right now the gunner counts down in draw
, not in update, so we’ll surely have a timing problem across devices. Still not gonna deal with that now.
I’ll give it a starting count of 60, display X1 down to 45, x2 down to 30, then nothing until zero. That will simulate the pause between lives.
I started with this:
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 > 45 then
sprite(GunnerEx1, Gunner.pos.x, Gunner.pos.y)
elseif Gunner.count > 30 then
sprite(GunnerEx2, Gunner.pos.x, Gunner.pos.y)
end
Gunner.count = Gunner.count - 1
if Gunner.count <= 0 then Gunner.alive = true end
end
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
It looks nearly OK:
I checked a video of the original, and noticed a couple of things. First, the explosion stages flicker back and forth a few times, not just 1 then 2 then done. Second, the bombs make an explosion when they hit, and I think that explosion is what damages the shield, that is, the bits that are on in the explosion turn off the bits in the shield. There’s no question that the damage is more random than what I’ve got now.
Finally, doing the tick in draw definitely makes the timing very fast, so we’ll need to move it to update time. But first let’s make it flicker.
To get that effect, I’m initializing Gunner.count to 120 (draw cycles) and processing it like this:
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 > 90 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 Gunner.alive = true end
end
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
Now what about the lives? Can I “just” process those right there where I set the gunner back to alive? I think so, let’s try this:
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 > 90 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
Gunner.alive = true
end
end
end
popStyle()
popMatrix()
Gunner.pos = Gunner.pos + GunMove
end
That just about works. There is this odd thing where the gunner explodes (again) when it’s dead. We find that and change it:
function Bomb:killsGunner()
if not Gunner.alive then return false end
local hit = rectanglesIntersectAt(self.pos,3,4, Gunner.pos,16,8)
if hit == nil then return false end
return true
end
Testing, I notice that you get an extra gunner, not just the 2. That’s to do with the handling of the fact that the count says 3, and the guns remaining is two. You have three lives, counting the one that’s playing. So we need to fix this.
This gives me the effect that I want:
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 > 90 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
That’s messy but seems to be correct. The number of gunners displayed below the line is one less than the number of lives you have, because you’ve got the one that’s above the line. I found a number of ways to make that code do odd things, including one version that counted lives down into the negative numbers very rapidly. That was amusing.
The fact that this logic is tricky makes me pretty sure that this “design” isn’t what we need, but it does seem to work. I’m still not happy with the delay between the explosion and the new gunner, so I’ll push those numbers up a bit.
Initializing to 240 and ending the explosion at 210 looks pretty good. One more thing seems fun:
function drawStatus()
pushStyle()
stroke(0,255,0)
strokeWidth(1)
line(8,16,216,16)
textMode(CORNER)
fontSize(10)
text(tostring(Lives), 24, 4)
text("CREDIT 00", 144, 4)
tint(0,255,0)
local addr = 40
for i = 0,Lives-2 do
sprite(asset.play,40+16*i,4)
end
if Lives == 0 then
textMode(CENTER)
text("GAME OVER", 112, 32)
end
popStyle()
end
That code at the bottom displays GAME OVER where the gunner would be if you had another one. Nearly good:
I think that’s a wrap. Commit: Three lives and out.
Let’s reflect.
Reflection
That’s roughly what I see when I look at my reflection. No haircut since February sometime. I think I’m going for a ponytail. But that’s not really what I meant by reflection.
Today has gone nicely. I did have to do some visual tuning to get the timing right. And it was good that I looked at the old game to see that the gunner explosion flickers between states like it does. We’re trying to replicate its look and feel to the extent that we can reasonably do so.
The code in the Gunner display logic looks rather more like the old-style assembly code than I’d like. We did things in those days that we don’t need to do now, and therefore shouldn’t do.
The Gunner drawing function mixes drawing the gunner, drawing the explosion, timing the explosion, moving the gunner, and counting lives. (But it doesn’t handle game over when the lives are used up: it just doesn’t display anything.)
When we used to write these things in assembler (and I did write Spacewar in PDP-1 assembler), once we started doing something with an “object”, we tended to do everything, so the code often turned out to be just one darn thing after another. There’d be subroutines for capabilities we could use more than once: that saved space. But we’d rarely if ever create a new function just because we were doing something different from what we had been doing on the previous line. We might put in a comment like “now see if we’re still alive”, but that was about it..
That made those programs hard to maintain for the next person, or the same person whose memory had faded. I’m having great difficulty understanding the Space Invaders assembly, between that style of programming and the fact that I don’t know its assembly language very well.
Today, with computers that are vastly more powerful, we have the ability to lean much further toward making the code more readable, which we do with more functions, more objects, and with actively refactoring for clarity when things start getting like they are in this program.
But it’s working, isn’t it?
As an experiment, let’s keep pushing forward without so much restructuring, to see what we get. Then we’ll review as much mess as there is, and refactor to something that at least one of us considers nicer.
Along the way, we’ll be watching for places where the mess makes us go slower. There is absolutely no doubt in my mind, based on literally decades of doing this TDD/refactoring thing, that the clear code lets me go faster. But we’re in no hurry. We’re here to see what happens.
Stay tuned! See you tomorrow!
-
Probably the stupidest thing I ever did was to buy an old Lamborghini. ↩