Space Invaders 23--Some work, some play.
Today we’ll try to fix the collision logic to accommodate our origin changes, and to make the invaders explode.
I’m imagining that the collision logic will be easy, but of course it has to be done, and I’m thinking that making the invaders explode will be a bit of a treat after the tedium of collision arithmetic.
I could be quite wrong in that imagining: I frequently am.
Now we’ve already made the shield collisions at least approximately correct, so first I’ll review how they work. I expect that this is going to tick me off at least somewhat.
The bomb-shield collision code is this:
function Bomb:update(army, increment)
self.pos.y = self.pos.y - (increment or 1)
if self:killedShield() then
army:deleteBomb(self)
return
end
if self:killsGunner() then
army:deleteBomb(self)
Gunner:explode()
return
end
if self.pos.y < 0 then
army:deleteBomb(self)
return
end
end
function Bomb:killedShield()
for i,shield in ipairs(Shields) do
local pos = shield.pos
local img = shield.img
local hit = self:damageShield(img,pos)
if hit then return true end
end
return false
end
function Bomb:damageShield(img, shieldPos)
local hit = rectanglesIntersectAt(self.pos,3,4, shieldPos,22,16)
if hit == nil then return false end
self:applyDamage(img, hit, shieldPos)
return true
end
function rectanglesIntersectAt(bl1,w1,h1, bl2,w2,h2)
local tr1 = bl1 + vec2(w1,h1) - vec2(1,1)
local tr2 = bl2 + vec2(w2,h2) - vec2(1,1)
if bl1.y > tr2.y or bl2.y > tr1.y then return nil end
if bl1.x > tr2.x or bl2.x > tr1.x then return nil end
mark = bl1
return bl1
end
Well. That takes a while to drill down, but I had forgotten that we have that lovely rectanglesIntersect
function. It returns, for some reason lost to antiquity, the origin of its first argument. It seems also to be storing the value in mark
, which seems to be a global. Bizarre. Let’s see what that’s about.
According to search, there is exactly one reference to mark
. I think we can do without that.
Now about that returning of the first point. I think there is an idea behind that, hidden in the name of the function, rectangles intersect at. The intention, for the future, was that in the case of shields, we will ultimately be doing incremental damage to them and detecting collisions only where they are still undamaged.
Be that as it may, I don’t like it. There are only two uses of the function, as above and:
function Bomb:killsGunner()
local hit = rectanglesIntersectAt(self.pos,3,4, Gunner.pos,16,8)
if hit == nil then return false end
return true
end
That one is particularly bizarre, since it has to convert to boolean.
I also have a vague feeling that the bomb-gunner interaction isn’t quite right. Anyway, I’m going to change that function to rectanglesIntersect
returning a boolean, then use that to implement rectanglesIntersectAt
. Then I can use the inner function when it makes more sense.
This first bit is a refactoring, pure and simple:
First copy/paste/edit:
function rectanglesIntersect(bl1,w1,h1, bl2,w2,h2)
local tr1 = bl1 + vec2(w1,h1) - vec2(1,1)
local tr2 = bl2 + vec2(w2,h2) - vec2(1,1)
if bl1.y > tr2.y or bl2.y > tr1.y then return false end
if bl1.x > tr2.x or bl2.x > tr1.x then return false end
return true
end
Then gut the At
version and call our new one:
function rectanglesIntersectAt(bl1,w1,h1, bl2,w2,h2)
if rectanglesIntersect(bl1,w1,h1, bl2,w2,h2) then
return bl1
else
return nil
end
end
This seems to work as before, so commit: refactor to rectanglesIntersect.
I noticed what seems like an anomaly. Two, actually. At one point, the fall of bombs definitely seemed to slow down, as if updates had slowed. (I had set the bomb probability rather high, because I wanted to watch them hit the gunner.) I’ll have to look into that.
More interesting was that it appeared that a bomb very near the left edge of the gunner could hit it. It’s hard to be sure, but certainly the bulk of the bomb was off to the left.
It could be a graphical glitch, a function of just how a rectangle so small gets drawn. Here’s what we have now:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
rect(self.pos.x,self.pos.y, 3,4)
popStyle()
end
Bombs actually have three different types, and they are animated. I don’t think I want to divert long enough to animate them just now, but I do think it’s prudent to at least draw one sprite.
First, this:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
sprite(xxx,self.pos.x, self.pos.y)
popStyle()
end
Now I can just tap on the xxx and select a bomb pattern. The plunger one has one version with the plunger bit right at the bottom, and that seems the easiest to watch.
I started with plunger1
and got lucky, it’s the one I wanted:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
sprite(asset.plunger1,self.pos.x,self.pos.y)
popStyle()
end
And it looks like this:
Watching that, I’m pretty sure what I’m seeing. It appears to me that if the plunger is exactly adjacent to the left edge, it will trigger a collision. I felt like it just barely missed and shouldn’t count. Let’s do a test to see what that rectangle intersection code does:
We do have this test, which indirectly tests the rectangle intersection code:
_:test("Bomb hits gunner", function()
Gunner = {pos=vec2(50,50)}
-- gunner's rectangle is 16x8 on CORNER
-- covers x = 50-65 y = 50,57
local bomb = Bomb(vec2(50,50))
-- bomb is 3x4, covers x = 50-52 y = 50-53
_:expect(bomb:killsGunner()).is(true)
bomb.pos.y = 58
_:expect(bomb:killsGunner()).is(false)
bomb.pos.y = 57
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 48 -- covers x = 48,49,50
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 47 -- 47,48,49
_:expect(bomb:killsGunner()).is(false)
bomb.pos.x = 65 -- 65,66,67
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 66 -- 66,67,68
_:expect(bomb:killsGunner()).is(false)
end)
These two tests really seem to be the ones I want:
bomb.pos.x = 48 -- covers x = 48,49,50
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 47 -- 47,48,49
_:expect(bomb:killsGunner()).is(false)
Our gunner is at x = 50-65. The first bomb there hits 50 and should kill. The second just slides by and should not.
Those tests are running. Perhaps my eyes deceive me. I see no point to writing a direct test on the rectangle code. We just reviewed how it’s used and there’s no slippage there that I can see.
I’m going to rain down bombs, but slow them down, and watch carefully.
Doing that convinces me that things are OK. It’s just that we’re still treating the gunner as a 16x8 rectangle, and so the bombs are triggering at the top edge. If we were checking for an actual collision, I think it would work fine.
Break for chai … less than a half hour there and back. Nom. But now I must digress.
Estimation Digression
As I was zipping over to the *$ for my chai (top down, temp about 60, wind in my hair, Jimmy Buffett on the tunes), I was reflecting on what we’ve seen already this morning.
My plan was (and is) to correct the missile targeting on the invaders. I’m quite sure that this will be a simple change, probably using the rectangle code. I’m aware that invaders are wider than they look, 16x8 I think, but with 8 bits of black. They were made that way to save arithmetic in the original game, I suspect, just blit the next invader onto the screen and move on. Or maybe they were going to be fatter, we don’t really know.
In any case, I expect the changes to be pretty easy.
If we were estimating stories, I’d give this one a nice low estimate, like one. (One what? One.)
We’ve been working for almost half our allotted time, and we haven’t even started on the invader targeting. We reviewed the existing code, which wasn’t really in my original thinking, and we found two things odd about it. We looked into the one and removed the global. We observed that the code for rectangle intersection had a weird interface and that neither of the two usages of it felt quite right. So we refactored the rectangle code to make it better.
We observed the code running and saw what seemed to be an anomaly in targeting. We checked it out, decided that it wasn’t a defect.
None of this stuff was in our estimate. We did those things because they were the right thing to do, to the best of our understanding.
If I had been under pressure to get done quickly, or pressure to hit my estimate, I’d have been less likely to check into those problem areas, less likely to improve the code.
The existence of an estimate tends to create a time constraint. The existence of a time constraint tends to limit our work so that we meet the time constraint. Limiting the work makes the product worse, and, quite quickly, slows us down a lot more because of defects and code friction.
This is why the famous “No Estimates” people object to estimation, and they are not wrong. Estimates are always waste, unless you can sell them. They are often unnecessary waste, especially in the hands of a solid team with all the necessary skills including deciding what to do next.
Estimates are razor blades. They should be used with extreme caution, and only when really needed.
But I digress. Let’s look at those missile-invader collisions.
Missile-Invader Collisions
In Main, we have this:
function drawMissile()
if Missile.v == 0 then return end
rect(Missile.pos.x, Missile.pos.y, 2,4)
Missile.pos = Missile.pos + vec2(0,0.5)
if TheArmy:checkForKill(Missile) then
Missile.v = 0
end
end
Remember that I said some of this code would probably tick me off? Well, here it is. We don’t have much of a “thing” for the missile, just a table with an element named v
. What even is that v
?
Well it seems to mean that the missile is alive if it’s 1 and not if it’s 0:
function fireMissile()
Missile.pos = Gunner.pos + vec2(0,5)
Missile.v = 1
end
So that’s weird. Anyway, we’re here to deal with checkForKill
, but there is another issue here. If you tap the screen rapidly, the missile resets down to the bottom of its path. I think it’d be better if it just didn’t fire.
Again, if I were on the clock, I wouldn’t fix this, but I’m here to make the product better so let’s make fireMissile
do nothing if Missile.v
is already 1:
function fireMissile()
if Missile.v == 0 then
Missile.pos = Gunner.pos + vec2(0,5)
Missile.v = 1
end
end
Super. Now you can’t fire a missile if there is one still flying. We may want to provide for more than one in flight, but that’s a new story. Now to checkForKill
:
function Army:checkForKill(missile)
for i, invader in ipairs(self.invaders) do
if invader:killedBy(missile) then
return true
end
end
return false
end
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
return true
else
return false
end
end
function Invader:isHit(missile)
if not self.alive then return false end
local missileTop = missile.pos.y + 4
local invaderBottom = self.pos.y - 4
if missileTop < invaderBottom then return false end
local missileLeft = missile.pos.x-1
local invaderRight = self.pos.x + 4
if missileLeft > invaderRight then return false end
local missileRight = missile.pos.x + 1
local invaderLeft = self.pos.x - 4
return missileRight > invaderLeft
end
Well, that goes all over but it makes sense. We have little choice but to check all the invaders, because we don’t have a data structure that lets us index into them by position. (And we probably never will have.)
Clearly what that isHit
is doing is a rectangle check. Let’s rewrite isHit
to use our rectangle code.
I recall that the invader per se starts 4 pixels in from the sprite position, and that it’s only 8 pixels wide. I’ll give the rectangle checker suitable values, which I think are these:
function Invader:isHit(missile)
if not self.alive then return false end
return rectanglesIntersect(missile.pos,2,4, self.pos+vec2(4,0),8,8)
end
I’ll give that a test run and see what happens. Well, one thing that happens is that I can’t fire a second missile. The first one’s v
must never have gone to zero?
I never noticed that before, because I wasn’t conditioning the shot based on it. Hmm. In draw, we do the update:
function drawMissile()
if Missile.v == 0 then return end
rect(Missile.pos.x, Missile.pos.y, 2,4)
Missile.pos = Missile.pos + vec2(0,0.5)
if TheArmy:checkForKill(Missile) then
Missile.v = 0
end
end
Perhaps it would be good not to let Missile.pos
increase to infinity.
function drawMissile()
if Missile.v == 0 then return end
rect(Missile.pos.x, Missile.pos.y, 2,4)
Missile.pos = Missile.pos + vec2(0,0.5)
if Missile.pos.y > 256 or TheArmy:checkForKill(Missile) then
Missile.v = 0
end
end
I notice two things. First, the missile isn’t firing from the center of the gunner. Let’s fix that:
function fireMissile()
if Missile.v == 0 then
Missile.pos = Gunner.pos + vec2(0,5)
Missile.v = 1
end
end
Sure enough, we didn’t adjust this when we changed to CORNER mode.
I tried this:
function Invader:isHit(missile)
if not self.alive then return false end
return rectanglesIntersect(missile.pos,2,4, self.pos+vec2(4,0),8,8)
end
Sometimes the missile clearly hits the invader and does not score. A look at the invader bitmap tells me that my recollection is wrong. The invaders are 16x8 alright, but they only have two blank pixels on each side. So:
function Invader:isHit(missile)
if not self.alive then return false end
return rectanglesIntersect(missile.pos,2,4, self.pos+vec2(2,0),12,8)
end
That looks more like it. I do notice another issue. The top row invaders are definitely smaller than the bottom rows, and the middle two row invaders might be a bit smaller as well. Here’s a pic from article 3, showing how the invaders look.
We may well want each invader to know her own rectangle. We could go so far as to use bit intersection when we build that, but I think that might be overkill for the invader collisions. For now, we’ve accomplished our first goal, fixing collisions. I think they are all good now.
Commit: collisions work in CORNER mode.
What Now?
My original thinking was to make the invaders explode now, as a little treat. Let’s go ahead with that.
There’s just one bitmap for this, alien_exploding
. Let’s begin by trying it for just one move cycle, and see how it looks.
Where’s the invader death code now:
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
return true
else
return false
end
end
And draw:
function Invader:draw()
if self.alive then
sprite(self.sprites[self.picture+1], self.pos.x, self.pos.y)
end
end
Hmm. We need to have the invader show as not alive immediately. But there’s a new state, maybe exploding
that occurs once and only once. Let’s try this:
function Invader:draw()
if self.alive then
sprite(self.sprites[self.picture+1], self.pos.x, self.pos.y)
elseif self.exploding then
sprite(asset.alien_exploding,self.pos.x,self.pos.y)
self.exploding = false
end
end
And this:
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
self.exploding = true
return true
else
return false
end
end
And this:
function Invader:init(pos, sprites)
self.sprites = sprites
self.pos = pos
self.alive = true
self.exploding = false
self.picture = 0
end
Best I can tell, the explosion is happening, but it’s so fast that we can’t see it. Let’s make it a count. Maybe 3,
That took some tuning. It takes 55 cycles to move all the aliens, and just less than a second if they’re all there. So I wound up with 15 as a good figure that puts the explosion on screen long enough to notice but short enough to look OK.
The code now is:
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
self.exploding =15
return true
else
return false
end
end
function Invader:draw()
if self.alive then
sprite(self.sprites[self.picture+1], self.pos.x, self.pos.y)
elseif self.exploding > 0 then
sprite(asset.alien_exploding,self.pos.x,self.pos.y)
self.exploding = self.exploding - 1
end
end
So yeah. Commit: invaders explode. Here’s a movie showing that. I forgot to make one at this point, so it also shows the fancy bombs we added in below.
It’s nearly 10 AM, morning session could be over. But I’m tempted to animate the bombs.
There are three kinds of bombs, plunger, rolling, and squiggle. Each has four bitmaps. Let’s just init the bombs with a table of tables of bomb types …
But no. We don’t want to read the images in on every bomb creation, so we need to set them up somewhere. For now, I’ll make a global table in Main. This is not going to be good code. Super, it will give us something to clean up.
function setup()
runTests()
createShields()
createBombTypes()
TheArmy = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
invaderNumber = 1
end
We code by intention, createBombTypes
. And implement:
function createBombTypes()
BombTypes = {
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
end
So that’s ugly but simple. For now, let’s create the bombs with a random type:
function Bomb:init(pos)
self.pos = pos
self.shapes = BombTypes[math.random(3)]
self.shape = 1
end
For non-Lua folks: conveniently, math.random
with an integer parameter returns an integer in the range [1,whatever]. In our case, 3.
So that should be good. Now we have to display the right shape:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
sprite(self.shapes[self.shape],self.pos.x,self.pos.y)
popStyle()
end
And we’d best update the shape when we move … meh. OK, this is slightly nasty, but I want to reach the end before my grip loosens:
function Bomb:update(army, increment)
self.pos.y = self.pos.y - (increment or 1)
self.shape = self.shape + 1
if self.shape > 4 then self.shape = 1 end
if self:killedShield() then
army:deleteBomb(self)
return
end
if self:killsGunner() then
army:deleteBomb(self)
Gunner:explode()
return
end
if self.pos.y < 0 then
army:deleteBomb(self)
return
end
end
I really want to convert to use mod. But I expect this to work.
And it does, but it’s perhaps too fast. I can hardly see the motion, and I suspect it hardly shows up at all in a movie:
Maybe they’re OK. I want to double check and make sure I’m not moving on every 120th instead of every 60th though.
Ah. I find this:
function Army:draw()
pushMatrix()
pushStyle()
for i,invader in ipairs(self.invaders) do
invader:draw()
end
for b,bomb in pairs(self.bombs) do
b:update(self)
b:draw()
end
end
That’s updating on every draw cycle. Let’s move that bomb updating into the update cycle:
function Army:doUpdate()
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
function Army:draw()
pushMatrix()
pushStyle()
for i,invader in ipairs(self.invaders) do
invader:draw()
end
for b,bomb in pairs(self.bombs) do
b:draw()
end
end
And what’s with the push and no pop. No good can come of that!
function Army:draw()
pushMatrix()
pushStyle()
for i,invader in ipairs(self.invaders) do
invader:draw()
end
for b,bomb in pairs(self.bombs) do
b:draw()
end
popStyle()
popMatrix()
end
Now let’s see how those bombs look:
Better. Commit: bombs animated.
Bombs can start from any invader, so sometimes they seem to fall though the lower ranks. I plan to worry about that at some future time. For now, we have actually exceeded our goal for the morning. Sweet. Let’s sum up.
Summing Up
We set out to fix targeting and do the alien explosion, and we accomplished that, some code cleanup, and even added in bomb types and animation.
There’s more to do on bomb types. In particular, the squiggle ones are always dropped right over the gunner. We’ll deal with that in due time. There are other arcane rules about drops, as well.
We found some issues and some odd code, and we fixed most of the issues we noticed and cleaned up at least some of the code. I feel very sure that if we’d been on the clock and under pressure, we’d have skipped the issues, maybe writing them on a card or making a mental note, and we’d have skipped cleaning up the code.
I’m sure we’d not have done the bomb animation, because there’d be a story for that and we’d be wanting to get time credit for that story. The dark side of recording estimates and actuals.
We’ve also created a new global variable, not exactly the best possible thing, but in Codea there is often no better thing to do. I sometimes put local
variables in tabs, but those are really compiled as globals when they’re outside of any code structure. We could make an object to hold things like that, and perhaps we will.
There is a lot of procedural code in Main
tab, including shields, which aren’t objects at all, and the gunner and its missile. This code is a bit messy to navigate with all those ideas mushed together in one tab. It’s only 160 lines, but it is an example of the trouble that leads to 1000 line modules and higher if left alone. So we’ll probably look for ways to improve that code as we go forward.
We are facing technical debt. Technical debt is the difference between the design we have, and the design we now could have, given what we know now. And that difference causes friction in development, and that slows us down. Of course, smoothing out that code also slows us down a bit, and we can always tell ourselves that it’s not that bad and we’re nearly done anyway.
That’s a slippery slope, but the temptation is real. We’re so close to having a playable game and it’ll surely take literally hours to factor out some of that code. I can easily imagine just writing the final article saying well, we could do those things but we’re done now so what the heck.
The risk in the real world is that this program will come under extended maintenance, people will be asking for new features and tweaks, and we will be slowed down forever.
The best fix for this problem, in my strong and rather experienced view, is to fix problems as early as we can, when we spot them, while they are still small. The longer we wait, the bigger the job seems, and the less it seems worthwhile because we keep thinking we’re nearly done anyway.
There’s no real way to fight those feelings. The thing is to never feel them, which we accomplish by keeping the code clean as we go. When we work in clean code every day, we go faster overall. Working faster is good for everyone. Rushing, or working in a messy field, is good for no one.
We have an opportunity to clean up the shields soon, because we have to do that nibbling away thing, and we’ll take the time to make the design better when that happens. We’ll have a similar opportunity with the gunner, and we may take that occasion to do the cleanup there. But we may not … because the gunner’s new capability is much simpler, we just need to detect collisions more accurately, not nibble away at him.
We’ll see. Will we succumb to temptation, or do the right thing? Can we even know what the right thing is? No. We can just do our best.
See you next time!