Space Invaders 15: A Question of Style
A friendly demo of shield damage techniques leads to an examination of an interesting coding approach. I need to figure out what I think about it.
To my credit, if not to his, Dave1707 from the Codea forum follows these articles and my occasional questions on the forum, and he has offered ideas and techniques a number of times. I’ve written a few times about how the invader missiles seem to destroy the shields by just running into them, and Dave has written a couple of demonstration programs showing ways we might make missiles seem to nibble away randomly at the shields.
His most recent demo used a coding approach that I find interesting, and I felt that I should give it a bit of serious consideration. So, this morning, you can help me look at and think about it.
I’ve put his entire demo program at the end of this article, and I’ll make a movie of it as well. But the thing I want to think about is how he handled whether an invader missile has hit a shield.
Here’s the code he uses, bit by bit. Here’s the draw
function for a shield:
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:shieldRange(b)
self:shieldHit(b)
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
end
end
This code displays the current shield as a sprite, which is basically just a bitmap. Then we have these lines:
for a,b in pairs(missileTab) do
self:shieldRange(b)
self:shieldHit(b)
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
end
OK, that’s a loop over all the missiles (missileTab
means “missile table” in Dave’s notation). And for each missile, it does this:
self:shieldRange(b)
self:shieldHit(b)
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
We can see that the last if nest is just deciding whether to use a circ(le) explosion or a rec(tangular) one, so we could refactor that this way:
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:shieldRange(b)
self:shieldHit(b)
self:explode(a,b)
end
end
function shields:explode(a,b)
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
end
For super clean code, I would actually do (at least) that refactoring, but some would certainly disagree. I’d so more than that on a fanatical day. But that’s not what we’re here to think about. It’s this:
self:shieldRange(b)
self:shieldHit(b)
self:explode(a,b)
What’s that about? We seem, at this level, to be unconditionally doing whatever shieldRange
, shieldHit
, and explode
do. It looks almost as though every shield will explode, every time.
Of course, that’s not the case, because in those other methods, we find, for example:
function shields:shieldRange(b)
if b.mx+missileWidth/2>=self.left and
b.mx-missileWidth/2<=self.right and
b.my+missileHeight/2>self.bottom and
b.my-missileHeight/2<self.top then
self.withinRange=true
end
end
Hm. This code checks to see whether the missile is positioned inside the shield, and if it is, sets withinRange
to true. Interesting … let’s look on:
function shields:shieldHit(b)
if self.withinRange then
for x=-missileWidth/2,missileWidth/2 do
for y=-missileHeight/2,missileHeight/2 do
xx=(b.mx-(self.left)+x)//1
yy=(b.my-(self.bottom)+y)//1
if xx>=1 and xx<=self.width and yy>=1 and yy<=self.height then
r1,g1,b1=self.shield:get(xx,yy)
if r1+g1+b1>0 then
self.hit=true
self.withinRange=false
return
end
end
end
end
end
end
Well, the big reveal, of course, is that if statement at the top. If we have previously found that the missile is inside the rectangle of the shield, we do this more complex thing. I’ll spare you analyzing the details: what that inner code does is that it fetches each pixel of the shield that is covered by the current missile position, and it if finds a non-zero pixel, it clears withinRange
and sets hit
to true.
Even more interesting. So let’s look at one of the explosion ones, either one will be fine:
function shields:shieldExplodeCirc(a,b)
if self.hit then
local rad=15
local t=rad
if b.mv<0 then
t=-rad
end
xx=b.mx-self.left
yy=b.my-self.bottom+t
for xx1=-rad,rad do
for yy1=-rad,yy+rad do
if xx1^2+yy1^2<rad^2 then
self.shield:set((xx1+xx)//1,(yy1+yy)//1,0,0,0,0)
end
end
end
self.hit=false
table.remove(missileTab,a)
end
end
Aha! This code is conditioned by self.hit
, so whatever it does, it only does if the missile has actually hit at least one pixel of the shield. (What it does is clear a circlular hole in the shield, as you can see in the movie.)
What have we here? Well, yesterday I wrote about “tell, don’t ask”. Arguably, this is an example of that notion, perhaps taken to extremes.
Let’s agree that because the check for pixels is really time consuming, we would like to do a quick check first to see if the missile could possibly hit us. So the three stages, range, hit, explode, make sense from a performance viewpoint.
We might quibble about the names. Perhaps we’d feel better with something like:
self:checkRange()
self:checkHit()
self:explodeIfHit()
But I’m nearly certain that I’d have written something like this:
function shields:collision(index,missile)
if self:inRange(missile) and self:isHit(missile) then
self:explode(index,missile)
end
end
In fact, what we have right now in my version is this:
function Army:checkForKill(missile)
for i, invader in ipairs(self.invaders) do
if invader:killedBy(missile) then
return true
end
end
return false
end
So you can see the direction I’m headed in, with the simpler conditional. We will wind up needing something like Dave has done, because we want a missile to fly right through a hole in a shield if it doesn’t touch a live pixel.
So this is a long warmup to address the question on my mind, which is
What about this style? Do we like it or hate it? When would it be a good idea? When might it not be a good idea? What’s up with this, anyway?
I guess that wasn’t one question, more of a question cloud. Point is, this is a style of coding that we could consider. Let me rewrite one of Dave’s methods like this:
function shields:shieldHit(b)
if not self.withinRange then return end
for x=-missileWidth/2,missileWidth/2 do
for y=-missileHeight/2,missileHeight/2 do
xx=(b.mx-(self.left)+x)//1
yy=(b.my-(self.bottom)+y)//1
if xx>=1 and xx<=self.width and yy>=1 and yy<=self.height then
r1,g1,b1=self.shield:get(xx,yy)
if r1+g1+b1>0 then
self.hit=true
self.withinRange=false
return
end
end
end
end
end
All I’ve done here is convert the outer if into a guard clause, which to my eyes does a better job of saying “we don’t do this if we aren’t within range”. Once we get used to guard clauses, they help us scan and grok the code quickly. Not in range? OK, move on, nothing to see here.
Now here’s an interesting thing. Refactoring to put in that guard clause gave me an idea for how one might code this whole thing.
If shieldRange
and shieldHit
were functions, returning a boolean, we could do it this way:
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:explodeIfHit(a,b)
end
end
function shields:explodeIfHit(a,b)
if not self:shieldHit(b) then return end
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
end
function shields:shieldHit(b)
if not self:shieldRange(b) then return false end
for x=-missileWidth/2,missileWidth/2 do
for y=-missileHeight/2,missileHeight/2 do
xx=(b.mx-(self.left)+x)//1
yy=(b.my-(self.bottom)+y)//1
if xx>=1 and xx<=self.width and yy>=1 and yy<=self.height then
r1,g1,b1=self.shield:get(xx,yy)
if r1+g1+b1>0 then
return true
end
end
end
end
return false
end
function shields:shieldRange(b)
if b.mx+missileWidth/2>=self.left and
b.mx-missileWidth/2<=self.right and
b.my+missileHeight/2>self.bottom and
b.my-missileHeight/2<self.top then
return true
end
return false
end
With suitable removal of the member state variables withinRange
and hit
, this code works.
So what have we here? Is it half of one, six a dozen of the other? I do like this aspect of what I just did:
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:explodeIfHit(a,b)
end
end
That expresses what we are accomplishing, exploding when hit, and hides the implementation. And once that phrase popped into my mind just now, it tells me why I like this way best. The other formulation looks like this:
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:shieldRange(b)
self:shieldHit(b)
self:explode(a,b)
end
end
Even with better names, perhaps checkInRange
and checkIfHit
, the code above reveals details of the implementation, first this then that then the other. But it doesn’t reveal all the details, because each step conditions the next step, using a couple of hidden variables in the object. And those variables have to be managed just so, turned on and off in just the right order.
Interesting. But is there a better case to be made for Dave’s choice? There might be.
Imagine an approach that’s a bit more collection oriented, a bit more “functional programming” in style. Or maybe we have a bunch of parallel processors sweeping over a large collection of invaders. Or maybe we’re programming this in APL or J.
We might have one function sweep over with inRange
and set just a few of the invaders to check for being hit. This function would cull out almost all the missiles and shields as not interesting. Then another function sweeps the remaining ones, and checks for a detailed hit. Again, the collection is culled. Finally, an exploder sweeps through and edits the few remaining shields and kills the corresponding missiles.
We happen to be processing each shield in a loop, checking each missile against each shield in another loop. If we had a more collection-oriented design, we might see more clearly why a discrete state for in range, for hit, and so on, could make sense.
This is quite interesting, I think. I could quibble over the names, and for now I do prefer the style above with just explodeIfHit
being called, with the implementation hidden inside. But there’s something interesting about the expanded form that Dave wrote. There’s more here than meets the eye.
But enough.
Summing Up
My first instinct when I saw Dave’s code was that I didn’t like it. But I knew that the way I’d do it, I’d wind up with a complex conditional (not very complex, admittedly), and conditionals are always worth looking at, especially as they get tricky.
Remember my clever code from yesterday? Conditionals and flags, right there. My tail is still sore where it bit me.
When someone offers a new idea, it’s often worth moving past our first impression to think about what the idea really is, where it might apply, and so on. And when we see code that looks odd, same thing. Check it out. Look into it.
Have we come out the same door we went in, in this case? I would say yes, but no.
I do expect that I’ll wind up with my usual style of code for missile-shield collisions, rather than the state-oriented approach that Dave took. But I will do so a bit more thoughtfully than I would have, and with an interesting alternative in mind. Right now, I thin of it this way:
What if a shield knew it needed to explode? What if a missile knew it was destroying a shield? What would that change in our implementation?
Objects are supposed to know things, and do things. That’s usually better than other objects knowing about them and making decisions for them.
Maybe there’s an interesting way to do more telling, and less asking, than our usual form.
We’ll find out. See you soon!
Oh! Here’s the movie I promised you, of Dave’s code running.
Dave’s Demo Code
displayMode(STANDARD)
function setup()
parameter.boolean("circ",false)
fill(255,0,0)
rectMode(CENTER)
spriteMode(CENTER)
missileWidth=10
missileHeight=20
missileTab={}
shieldTab={}
shieldImg={}
for s=1,5 do
shieldImg[s]=image(88,64)
setContext(shieldImg[s])
background(255)
setContext()
shieldTab[s]=shields(shieldImg[s],vec2(180*s,HEIGHT/2),88,64)
end
end
function draw()
background(0)
text("tap multiple times near the bottom or top",WIDTH/2,100)
for a,b in pairs(missileTab) do
rect(b.mx,b.my,missileWidth,missileHeight)
b.my=b.my+b.mv
if b.my>HEIGHT or b.my<0 then
table.remove(missileTab,a)
end
end
for s=1,#shieldTab do
shieldTab[s]:draw()
end
end
function touched(t)
if t.state==BEGAN then
v=5
if t.y>HEIGHT/2 then
v=-5
end
table.insert(missileTab,{mx=t.x,my=t.y,mv=v})
end
end
shields=class()
function shields:init(shld,pos,sw,sh)
self.shield=shld
self.center=pos
self.width=sw
self.height=sh
self.left=self.center.x-self.width/2
self.right=self.center.x+self.width/2
self.bottom=self.center.y-self.height/2
self.top=self.center.y+self.height/2
self.withinRange=false
self.hit=false
end
function shields:draw()
sprite(self.shield,self.center.x,self.center.y)
for a,b in pairs(missileTab) do
self:shieldRange(b)
self:shieldHit(b)
if circ then
self:shieldExplodeCirc(a,b)
else
self:shieldExplodeRec(a,b)
end
end
end
function shields:shieldRange(b)
if b.mx+missileWidth/2>=self.left and
b.mx-missileWidth/2<=self.right and
b.my+missileHeight/2>self.bottom and
b.my-missileHeight/2<self.top then
self.withinRange=true
end
end
function shields:shieldHit(b)
if self.withinRange then
for x=-missileWidth/2,missileWidth/2 do
for y=-missileHeight/2,missileHeight/2 do
xx=(b.mx-(self.left)+x)//1
yy=(b.my-(self.bottom)+y)//1
if xx>=1 and xx<=self.width and yy>=1 and yy<=self.height then
r1,g1,b1=self.shield:get(xx,yy)
if r1+g1+b1>0 then
self.hit=true
self.withinRange=false
return
end
end
end
end
end
end
function shields:shieldExplodeRec(a,b)
if self.hit then
for x=-missileWidth,missileWidth do
for y=1,15 do
for z=1,15 do
a1=math.random(-15,15)
b1=math.random(25)
if b.mv<0 then
b1=-b1-missileHeight/2
end
self.shield:set((b.mx-self.left+x+a1)//1,
(b.my-self.bottom+y+b1)//1,0,0,0,0)
end
end
end
self.hit=false
table.remove(missileTab,a)
end
end
function shields:shieldExplodeCirc(a,b)
if self.hit then
local rad=15
local t=rad
if b.mv<0 then
t=-rad
end
xx=b.mx-self.left
yy=b.my-self.bottom+t
for xx1=-rad,rad do
for yy1=-rad,yy+rad do
if xx1^2+yy1^2<rad^2 then
self.shield:set((xx1+xx)//1,(yy1+yy)//1,0,0,0,0)
end
end
end
self.hit=false
table.remove(missileTab,a)
end
end