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 drawfunction 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.

shields


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