Space Invaders 28
A bit more exploding. I’m going to continue to let things get messy.
I watched a video of the original game yesterday, and it looks to me as if the pattern of destruction that it leaves in the shields is exactly the pattern of the bomb’s explosion. I tried to figure out the assembly code that does the job but so far I’ve not cracked it. More study is required.
I did notice an interesting comment in the code. It says something to the effect that if a missile has collided and it’s not on an invader, then it must be that it shot down a bomb. If my thinking is correct that the original just detects collisions by seeing dots on the screen, that makes sense. But there’s no way I’d do something like that these days. Most interesting.
Anyway, the first thing I plan to do today is to read in the bomb explosion bitmap, and display it briefly when the bomb hits the shield. I am not planning to use it to mark the destruction, at least not yet. We’ll see where we go.
I mentioned yesterday that the code is getting messy, and that I plan to let that continue for a while. I suspect that we’ll encounter some discernible problems due to that decision. We’ll see.
OK, first thing is to read in this bitmap, and put it somewhere where an invader can get ahold of it. Why not just make it a member variable? It’ll save us creating a global. There’s no point being intentionally stupid.
function Army:init()
local vader11 = readImage(asset.documents.Dropbox.inv11)
local vader12 = readImage(asset.documents.Dropbox.inv12)
local vader21 = readImage(asset.documents.Dropbox.inv21)
local vader22 = readImage(asset.documents.Dropbox.inv22)
local vader31 = readImage(asset.documents.Dropbox.inv31)
local vader32 = readImage(asset.documents.Dropbox.inv32)
local vaders = {
{vader31,vader32},
{vader21,vader22}, {vader21,vader22},
{vader11,vader12}, {vader11,vader12}
}
local explosion = readImage(asset.alien_shot_exploding)
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 184-row*16)
table.insert(self.invaders, 1, Invader(p,vaders[row],explosion))
end
end
self.invaderNumber = 1
self.overTheEdge = false
self.motion = vec2(2,0)
self.updateDelay = 1/65 -- less than 1/60th by a bit
self:resetTimeToUpdate()
self.bombs = {}
end
Here I’ve read it in and passed it to each Invader. Now to receive it:
No, that’s wrong. The explosion belongs to the Bomb. We’ll need to put it in there somehow. Revert.
Things aren’t so nice over on the Bomb tab, but we do have the code that creates the global BombTypes:
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
For now, I’ll just add a new global BombExplosion. Not a great solution but a pragmatic one. I take back what I said about not doing intentionally stupid things. In fairness to me, this isn’t terribly stupid, just not really good. We should probably have the bitmaps all in some single class, or something like that.
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)}
}
BombExplosion = readImage(asset.alien_shot_exploding)
end
Now to do the actual exploding, we find the interesting action in our update function:
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
It seems to me that rather than immediately call deleteBomb
, we want to set an exploding counter for the draw
function, then count it down while displaying the explosion, and only after that’s done calling deleteBomb
.
Should be messy but straightforward. Let’s try displaying it for a count of 15 at first. First, we’ll posit an explode method. I’m going to pass in army
for the callback but I think we’re going to have to save it in a member variable anyway.
And I just remembered we’d best not update the bomb position while exploding. So …
function Bomb:update(army, increment)
if self.explodeCount <= 0 then
self.pos.y = self.pos.y - (increment or 1)
end
self.shape = self.shape + 1
if self.shape > 4 then self.shape = 1 end
if self:killedShield() then
self:explode(army)
return
end
if self:killsGunner() then
self:explode(army)
Gunner:explode()
return
end
if self.pos.y < 0 then
self:explode(army)
return
end
end
And for explode
…
function Bomb:explode(army)
self.army = army
self.explodeCount = 15
end
And then in draw
, we move from this:
function Bomb:draw()
pushStyle()
fill(255)
stroke(255)
sprite(self.shapes[self.shape],self.pos.x,self.pos.y)
popStyle()
end
To this …
function Bomb:draw()
if self.explodeCount > 0 then
sprite(BombExplosion, self.pos.x, self.pos.y)
self.explodeCount = self.explodeCount - 1
if self.explodeCount == 0 then army:deleteBomb(self) end
else
sprite(self.shapes[self.shape],self.pos.x,self.pos.y)
end
end
I deleted the unnecessary fill and stroke, and the push and pop since we’re not affecting style. I am irritated somewhat by these odd settings that require a transition on the last count. But I expect this to work, with about 60 percent confidence.
I should have gone lower. I get this error:
Bomb:27: attempt to index a nil value (global 'army')
stack traceback:
Bomb:27: in method 'draw'
Army:100: in method 'draw'
Main:29: in function 'draw'
That’s clearly a missing self
but I’m surprised because I didn’t see any explosion on the screen. I’ll make the change and watch again.
So that works nicely, with one exception. The explosion appears above the hole that we dig in the shield. That’s because we explicitly dig downward:
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
Let’s fudge the bomb downward by 3 pixels when we decide to explode and see how that looks.
function Bomb:explode(army)
self.pos.y = self.pos.y - 3
self.army = army
self.explodeCount = 15
end
That looks better but the explosion seems to be to the right of the hole.
Reviewing the code that imports the bitmaps, I find that the invader’s shot explosion is 6x8, while the shots themselves are 3x8. In a centered model, they’d come out about right. In our corner model, the wider explosion will be centered to the right of the shot. So we should fudge the position inward as well.
I’ll do that and then we need to talk.
function Bomb:explode(army)
self.pos = self.pos - vec2(3,3)
self.army = army
self.explodeCount = 15
end
This has a most curious effect: Some of the bombs cut big diagonal swaths through the shields, all in one go:
My guess is that we’re calling explode multiple times on the same bomb, and getting a new hit because we’ve moved it. We need not to consider bomb hits if we’re already exploding:
function Bomb:update(army, increment)
if self.explodeCount > 0 then return end
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
self:explode(army)
return
end
if self:killsGunner() then
self:explode(army)
Gunner:explode()
return
end
if self.pos.y < 0 then
self:explode(army)
return
end
end
We’ll just blow off the entire update if we’re exploding. And that does the trick. It looks pretty good now:
But …
We still need to talk
Now part of our work here was due to the need to adjust the position of the explosion relative to the bomb, and that was a direct result of our switching to CORNER
mode rather than CENTER
. We did that because we (well, I) didn’t want to have even-width objects centered in between pixels. My thinking was that positioning and bit matching was going to be easier that way.
But there are references to our explosion counter in four methods and at least seven references. That’s rather messy.
It’s all in place now, or at least in places, but the code is getting pretty messy by my standards. Most of the messiness is at least encapsulated inside the Bomb
class, but it’s still not nice. Take a look at the whole class. No need to read it, just kind of take it in:
-- Bomb
-- RJ 20200819
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)}
}
BombExplosion = readImage(asset.alien_shot_exploding)
end
Bomb = class()
function Bomb:init(pos)
self.pos = pos
self.shapes = BombTypes[math.random(3)]
self.shape = 1
self.explodeCount = 0
end
function Bomb:draw()
if self.explodeCount > 0 then
sprite(BombExplosion, self.pos.x, self.pos.y)
self.explodeCount = self.explodeCount - 1
if self.explodeCount == 0 then self.army:deleteBomb(self) end
else
sprite(self.shapes[self.shape],self.pos.x,self.pos.y)
end
end
function Bomb:update(army, increment)
if self.explodeCount > 0 then return end
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
self:explode(army)
return
end
if self:killsGunner() then
self:explode(army)
Gunner:explode()
return
end
if self.pos.y < 0 then
self:explode(army)
return
end
end
function Bomb:explode(army)
self.pos = self.pos - vec2(3,3)
self.army = army
self.explodeCount = 15
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
if not shield:hitBits(self.pos,3,4) then return false end
shield:applyDamage(hitPos)
return true
end
function rectanglesIntersectAt(bl1,w1,h1, bl2,w2,h2)
if rectanglesIntersect(bl1,w1,h1, bl2,w2,h2) then
return bl1
else
return nil
end
end
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
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
I’m not at all sure what I’d like to do about it, but it’s troubling me, especially since the bomb is an object, not just a blob of procedure. It has member variables and associated methods, and I’d expect it not to be such a mess.
I think we have here a combination of concerns.
First, we’ve just been hammering in functionality, trying to be fairly neat about it, but not really looking back to improve the code. By the nature of that approach, code gets messy.
Second, though, I suspect that we have some design ideas trying to appear. We have a bit of commonality in the detection of collisions, but I’‘m getting the feeling that there’s something to be discovered.
We are doing the same tricky thing with the countdown timers in the gunner, the invaders, and the bomb. The missiles have an explosion as well, so it’s a good bet we’ll be doing a similar thing there. This is definitely an abstract idea waiting to be understood and isolated.
Maybe all these objects need to have a common capability called selectSprite
or something, that encapsulates the various choices for sprites, timers, and so on. Or maybe that’s too much. It’s certainly speculative at this point.
I have a bad feeing about this code, and I don’t usually have that feeling. (Or it could be something I ate, I suppose.) I think the code is drifting out of control, and needs to be brought back into line.
For now, we have our bombs exploding, and it looks good on screen if not in the code.
One good thing: all our changes are in one tab. Commit: bombs explode.
See you next time!