Space Invaders 30
Let’s get a start on scoring. This should be easy enough.
The invaders know when they’ve been hit:
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:isHit(missile)
if not self.alive then return false end
return rectanglesIntersect(missile.pos,2,4, self.pos+vec2(2,0),12,8)
end
So if each invader knew her score, she could add it into the total score. The scoring is:
- Saucer - mystery
- top row invaders: 30
- middle 2 rows invaders: 20
- bottom 2 rows invaders: 10
We’ve already had a need for the invaders to know the army they’re in. Let’s check how they are set up and see how we cca give them their individual score.
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}
}
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]))
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
Creation is by row, from top to bottom. So I’ll add a score table, and create them with more information.
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 scores = {30,20,20,10,10}
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], scores[row], army))
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
And
function Invader:init(pos, sprites, score, army)
self.pos = pos
self.sprites = sprites
self.score = score
self.army = army
self.alive = true
self.exploding = 0
self.picture = 0
end
Then I’ll init a global Score in Main setup
and add to it here:
function Invader:killedBy(missile)
if not self.alive then return false end
if self:isHit(missile) then
self.alive = false
Score = Score + self.score
self.exploding =15
return true
else
return false
end
end
And to display it, at least for now, I’ll replace the CREDITS in status with SCORE:
function drawStatus()
pushStyle()
stroke(0,255,0)
strokeWidth(1)
line(8,16,216,16)
textMode(CORNER)
fontSize(10)
text(tostring(Lives), 24, 4)
text("SCORE " .. tostring(Score), 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
And it works. The various aliens correctly add to the score.
Commit: scoring for all three invader types.
But …
We need to talk
This change involved modifying:
- Main tab setup
- Main tab drawStatus
- Army tab creation of invaders
- Invader tab individual invader scoring.
Now it went in easily enough, but the code is sort of smushed in where it seems to fit. Every time we do this, things get a bit less smooth, the going feels a bit more rough and slower. Just a bit. But just a bit adds up over time, whether it’s improvement or deterioration. Right now, I think we’re looking at very slight deterioration.
Still, everything is broken out into at least sensibly-named functions, so it’s not entirely awful. But I’d hate to have to maintain several thousand lines like this.
OK enough whining. We should either do something about it, or get over it. Right now, I’m getting over it. I want to change that bottom line so that it can be destroyed, just for verisimilitude.
I’ll make a bitmap and draw it.
In Main setup
:
...
Line = image(208,1)
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
And in drawStatus
:
function drawStatus()
pushStyle()
tint(0,255,0)
sprite(Line,8,16)
...
The line is still there, but of course it’s entirely different now.
It should be possible to adopt the Shield:clearFromBitmap
to do this job. That looks like this:
function Shield:clearFromBitmap(tx,ty, source)
for x = 1,source.width do
for y = 1,source.height do
r,g,b = source:get(x,y)
if r+g+b > 0 then
self.img:set(tx+x-1, ty+y-1, 0,0,0,0)
end
end
end
end
Now we don’t even have an object for our line. And it’ll be inefficient to fetch all those bits and ignore most of them, but let’s see what we can do.
The line detection is here:
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 < 16 then
self:explode(army)
return
end
end
It’s that last bit there with the < 16. We’ll want to damage the line right here but I’ll call a function.
Well some serious cut and paste horror and we have this:
function Bomb:clearLineFromBitmap(bomb)
local relativePos = bomb.pos - vec2(8,16)
local tx = relativePos.x
local ty = relativePos.y
local source = BombExplosion
for x = 1,source.width do
for y = 1,source.height do
r,g,b = source:get(x,y)
if r+g+b > 0 then
Line:set(tx+x-1,ty+y-1,0,0,0,0)
end
end
end
end
I don’t like that damage, because it’s too regular. The original game had more of the scattered parts on the line. Cheating the hit position down by one makes it better.
After a bit of fiddling, I decide to randomly choose a downward cheat of 1, 2, or 3:
function Bomb:clearLineFromBitmap(bomb)
local relativePos = bomb.pos - vec2(8,16) - vec2(0,math.random(1,3))
local tx = relativePos.x
local ty = relativePos.y
local source = BombExplosion
for x = 1,source.width do
for y = 1,source.height do
r,g,b = source:get(x,y)
if r+g+b > 0 then
Line:set(tx+x-1,ty+y-1,0,0,0,0)
end
end
end
end
This code is horrendously inefficient, since it checks and sets all kinds of lines above and below the actual Line. But it works, and we can ship it.
But it isn’t “right”.
I’ll wrap this here and take it up again in the morning.
Thursday
I’ve decided to re-blurb this article “When is too much way too much?”
Yesterday at end of day I put the nifty explosion marks in the status line, because that happened in the original version. In the original version, it was a side effect of the way the display worked. Bitmaps in memory were mapped to the screen and if you erased a bit, it was gone from the picture. Explosions draw and erase themselves, and at least in some cases, do not restore what was there before.
In Codea, we explicitly draw whatever’s on the screen, one thing after another, with vector graphics and sprites. We’ve made our invaders look vintage by creating bitmaps from the ancient scrolls, and scaling them up to look all 8-bit graphics.
Anyway, the old game damages that line at the bottom, and I implemented that using the same approach I took in the shields, erasing the overlapping bits from the explosion.1
That’s too much emulation, too much homage. First thing this morning, I’m going to just erase some random bits in the Line
, rather than do all that explosion-copying rigmarole.
Here’s yesterday’s code:
function Bomb:clearLineFromBitmap(bomb)
local relativePos = bomb.pos - vec2(8,16) - vec2(0,math.random(1,3))
local tx = relativePos.x
local ty = relativePos.y
local source = BombExplosion
for x = 1,source.width do
for y = 1,source.height do
r,g,b = source:get(x,y)
if r+g+b > 0 then
Line:set(tx+x-1,ty+y-1,0,0,0,0)
end
end
end
end
First, let’s just forget that bitmap explosion stuff and remove a few random bits.
function Bomb:clearLineFromBitmap(bomb)
local relativePos = bomb.pos - vec2(8,16)
local tx = relativePos.x
for x = 1,6 do
if math.random() < 0.5 then
Line:set(tx+x-1,1, 0,0,0,0)
end
end
end
That’s good enough. We can clean it up a bit:
function Bomb:clearLineFromBitmap(bomb)
local tx = bomb.pos.x - 7
for x = 1,6 do
if math.random() < 0.5 then
Line:set(tx+x,1, 0,0,0,0)
end
end
end
The effect looks just as good, but now the name makes no real sense, so let’s rename it to, oh, damageLine
…
function Bomb:damageLine(bomb)
local tx = bomb.pos.x - 7
for x = 1,6 do
if math.random() < 0.5 then
Line:set(tx+x,1, 0,0,0,0)
end
end
end
We rename in the call as well, of course:
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 < 16 then
self:damageLine(self)
self:explode(army)
return
end
end
Now that code bothers me more than just a bit. It does the thing but it’s messy.
We know that we’re slowly making the code worse. Well, you know it, but it’s beginning to sink in on me as well.
I see two things. First, that updating on self.shape
is trying to make the value go 1,2,3,4 and cycle. Let’s change the use of the shape
member variable to be zero through three rather than one through four. We’ll use search to identify all the places we use it.
We init it to one. Change to zero:
function Bomb:init(pos)
self.pos = pos
self.shapes = BombTypes[math.random(3)]
self.shape = 0
self.explodeCount = 0
end
We index with it directly into the shapes table. Add one at the time of indexing:
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 + 1],self.pos.x,self.pos.y)
end
end
And make it cycle zero through three:
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)%4
if self:killedShield() then
...
Now those if blocks are bothering me. To the degree practical, a method should contain either one bit of computational, conditional or looping logic, or a series of calls to other methods.
I learned this “rule” from Kent Beck, in person and in his book “Smalltalk Best Practice Patterns”. Anything stupid that I say or do here is my fault, not his.
So we’ll extract that logic into killThings
.
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)%4
self:killThings(army)
end
function Bomb:killThings(army)
if self:killedShield() then
self:explode(army)
return
end
if self:killsGunner() then
self:explode(army)
Gunner:explode()
return
end
if self.pos.y < 16 then
self:damageLine(self)
self:explode(army)
return
end
end
This method may or may not kill things. Let’s rename it to checkCollisions
:
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)%4
self:checkCollisions(army)
end
function Bomb:checkCollisions(army)
if self:killedShield() then
self:explode(army)
return
end
if self:killsGunner() then
self:explode(army)
Gunner:explode()
return
end
if self.pos.y < 16 then
self:damageLine(self)
self:explode(army)
return
end
end
That suggests that we could extract the three inner bits into separate methods, and I’d really like to do that but we have those early returns: we can only collide once.
Just to see if I like it better, I’m going to go ahead with an extract, but it’ll be a tiny bit tricky.
No, wait! We can re-express this code with elseif
:
function Bomb:checkCollisions(army)
if self:killedShield() then
self:explode(army)
elseif self:killsGunner() then
self:explode(army)
Gunner:explode()
elseif self.pos.y < 16 then
self:damageLine(self)
self:explode(army)
end
end
Ha. That’s nicer. Those early returns are OK at the top of a method, but embedded ones are harder to spot.
Now how can we remove that duplicated call to explode
? I have an idea that’s a bit odd. Let’s see how it looks:
function Bomb:checkCollisions(army)
if self:killedShield() then
elseif self:killsGunner() then
Gunner:explode()
elseif self.pos.y < 16 then
self:damageLine(self)
else
return -- without exploding
end
self:explode(army)
end
A bit tricky? I think it’s not too bad, except for the empty if at the top. We do need it, or else we could call for shield damage from here. Right now that’s too much but I want to make the code a bit more clear that this empty if is OK.
function Bomb:checkCollisions(army)
if self:killedShield() then
-- intentionally blank
elseif self:killsGunner() then
Gunner:explode()
elseif self.pos.y < 16 then
self:damageLine(self)
else
return -- without exploding
end
self:explode(army)
end
This is working and more nearly right than it was. Strictly speaking, for symmetry we should call for the shield damage inside the killedShield
check, but that logic is a bit more intricate than I want to deal with in this step. I will, however, add a comment to the one above, as a reminder:
function Bomb:checkCollisions(army)
if self:killedShield() then
-- intentionally blank
-- would be better to move shield damaging up here.
elseif self:killsGunner() then
Gunner:explode()
elseif self.pos.y < 16 then
self:damageLine(self)
else
return -- without exploding
end
self:explode(army)
end
This seems to be as long as I can hold my breath. Commit: improve line damage, refactor bomb collisions.
This article seems long enough, let’s sum up.
Summing the Up
We began this session with a quick implementation of scoring. It’s not entirely bad: invaders know their score value and when they unfortunately pass into the world beyond, they deposit that value in a Score
variable, which is displayed by our displayStatus` function.
It’s not very centralized, but in a small program it seems pretty reasonable. I’m not entirely comfortable but it seems nearly in proportion to the problem.
Then I decided to damage the green line at the bottom. I think what happened was interesting.
First of all, it went in quite smoothly. I decided to use the bomb explosion bitmap to damage the line, and cut and pasted the shield damage code to do it. A bit of hammering and it looked good.
What was going on, though, wasn’t very good. The program was iterating over all the bits of the explosion, clearing bits in the line if they were set in the explosion. But since the line is only one pixel thick, and the explosion is 8 pixels high, 7/8 of that effort cleared bits outside the line, which are conveniently ignored by the setters and displayers. Wasteful.
In due time it came to me that that wasn’t the most clever thing I could have done, and that copying the details of the original game wasn’t a good idea here. Mind you, in the original, it really does clear all those bits, because it has to: otherwise the explosion would hang there forever.
So that was a quick change, just randomly killing a few bits in the line. It looks just as good as it did before.
Then a good thing happened. The name of the function, something about clearing bits from bitmap, was no longer good. So I changed it. That got me looking at the code that called it.
I spotted a couple of things. I didn’t like the code’s complex structure, and I noticed a clunky approach to cycling the bomb shapes. I fixed the cycling one, which was pretty easy.
Then I was on a roll. I extracted the collision-checking code, recognized that was a better name and used it. Then I noticed the early return structure and put together something that is at least in some ways better.
There’s still something odd about that return at the end to skip the exploding part, but it let me remove some duplication.
I noticed a lack of symmetry in the code that was not clear before. Each branch checks a condition and if it’s true, damages a collided object. Except that the shield code has the damage hidden down below and the others do not.
I left that asymmetry in and also left a sort of “to do” comment, which i rarely do, with a reminder of why that branch of the if was empty, and how it might be improved.
Each of the steps doing this was tiny, and each one left the program running just fine. We could have committed at any or every point along there, and could have left for the chai at any time. (Coming up soon, mind you.)
My good friend GeePaw Hill refers to “harvesting the value of small changes”. You’d do well to read his blogs and watch his videos. Even join his Camerata Slack if you can. (Join mine too.)
What we were doing here was exactly harvesting the value of small changes. The code in this area is better than it was before. We have reversed the flow of entropy that has been making our program slowly deteriorate. In this area, at least, it’s better than it was before.
Small changes. Harvesting their value. Good stuff.
See you next time!
-
As usual, I say “we” when things are going reasonably well and you’re my virtual pair programmer. (I often wish you’d talk more while this goes on, although that might be scary.) I say “I” when I’m talking about a mistake, since you aren’t very much to blame. You could have emailed the hint, but that seems too much to ask. ↩