Maybe make the invader bombs do some damage? Or what?

I’m not entirely sure which way to go at this moment. There are lots of graphical things to improve, moving away from these squares I’m using now, but while that affects the game’s appearance, it doesn’t really affect function. I think it’s best to improve function.

There are also some details of the original game that need to be scoped out and dealt with. I believe that only certain columns of invaders actually fire missiles, and that there is some official order of firing. And I’m pretty sure that the screen area they use is very different from what I’m doing right now. Maybe even their spacing.

But most of that will involve reading the original code, which isn’t interesting for articles, as far as I’ve been able to figure out. And, again, it may improve how good the game looks, but it isn’t as important as things like making the missiles and bombs work.

So. The bombs have two effects. When they smash into a shield, they eat away at it. And when they strike the gunner, they destroy it and either a new one spawns, or game over. We don’t have shields set up at present, and so let’s see about destroying the gunner.

This is a collision problem, of course, and we already have one collision implementation in place, the gunner’s missiles hitting invaders. Missiles are fired in the touched event, but handling it is done in the Main function drawMissile:

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

That code invites the Army, which knows all the invaders, to check whether the Missile has killed anyone:

function Army:checkForKill(missile)
    for i, invader in ipairs(self.invaders) do
        if invader:killedBy(missile) then
            return true
        end
    end
    return false
end

And the Army asks the invaders:

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

I see duplicate code there for not alive. This isn’t the time to worry about that.

More interesting is the isHit function, which is essentially checking to see if the missile intersects the invader. The invader defines itself as a rectangle four pixels in each direction from its center. This tells me that invaders had better be drawn in rectMode CENTER. A quick check ensures that they are.

Now the player missile is only one pixel wide. Invader bombs are three pixels wide, and they have interesting detailed shapes, varying as they fly. Here’s a pic including the shapes of the invader bombs:

bitmaps

Seems we’re moving toward needing a general-purpose rectangle intersection function. For now, I’ll resist that.

Let’s see what goes on now with Bombs:

function Bomb:draw()
    pushStyle()
    fill(255)
    stroke(255)
    strokeWidth(255)
    rectMode(CENTER)
    rect(self.pos.x,self.pos.y, 3,4)
    popStyle()
end

function Bomb:update(army, increment)
    self.pos.y = self.pos.y - (increment or 1)
    if self.pos.y < 0 then
        army:deleteBomb(self)
    end
end

(I just added that call to rectMode, and committed it.)

We could do something clever about only checking the bomb if it’s low enough, but if we do our job correctly that might drop out. With the other checker in mind, but without looking at it, let’s see what we can do. Something like this:

function Bomb:update(army, increment)
    self.pos.y = self.pos.y - (increment or 1)
    if self:killsGunner() then
        army:deleteBomb(self)
        Gunner:explode()
    end
    if self.pos.y < 0 then
        army:deleteBomb(self)
    end
end

The Gunner does happen to be a global right now, so we can talk to it. It’s not an object, however, so we can’t really send it a message. I’m assuming that it will be an object very soon. We’ll see what happens.

Now to check the kill. Also I want to make sure that the Gunner is also drawn in CENTER mode. Yes.

I have to digress for a moment. The real Gunner looks like a wide platform with a turret in the middle.

gunner

An ideal solution matching the original game would be to match the bits in the bomb against the bits in the gunner and if there is an actual collision, count it. I’m going to skip that for now. The “player” as it is called in the real game, is 16 pixels wide and 8 high. I’ll use that size for now and maybe even change our picture to match.

I just typed this in:

function Bomb:killsGunner()
    -- some of these must be off by one?
    local gunnerTop = Gunner.pos.y + 4
    local bombBottom = self.pos.y - 2
    if bombBottom > gunnerTop then return false end
    -- missile is at the gunner level
    local gunnerLeft = Gunner.pos.x-8
    local bombRight = self.pos.x + 1
    if bombRight < gunnerLeft then return false end
    -- we have hit him or are on his right
    local gunnerRight = Gunner.pos.x+8
    local bombLeft = self.pos.x - 1
    if bombLeft > gunnerRight then return false end
    return true
end

It’s surely wrong but I don’t have a test. (I should have, and I’ll write one in a moment, but right now I can’t resist running this. If it actually worked, then if I position the gunner under a missile I should get an error calling explode on the Gunner table.

Bomb:21: attempt to call a nil value (method 'explode')
stack traceback:
	Bomb:21: in method 'update'
	Army:76: in method 'draw'
	Main:25: in function 'draw'

Take that, unbelievers! Worked the first time. Let’s do write some tests, however.

It’s so easy to fall into the trap of just running the code to see if it works. And if it does, it’s easy to decide not to write tests for it. If it fails, it’s easy to decide to debug it until it works and then not write a test. All those things are risky in my view, especially when dealing with something a bit tricky and critical, as this is. And this is even more true when we’ll probably want to refine that code later.

So some tests:

Hmm. A hairy yak has appeared. If I’m going to test this properly, I need to know what the actual coordinates of my object are. So I think I’d like to put some colored points around a missile and the gunner, to see what the actual numbers are.

Another yak has appeared. One of my tests isn’t running. The check for killGunner has been added, and that checks the global Gunner, which the “bombs get deleted” test does not set up. Fix that:

        _:test("bombs get deleted", function()
            Gunner = {pos=vec2(1000,1000)}
            local army = Army()
            local bomb = Bomb(vec2(122,10))
            army:dropBomb(bomb)
            _:expect(countTable(army.bombs)).is(1)
            for i = 1,10 do
                bomb:update(army,1)
                _:expect(countTable(army.bombs)).is(1)
            end
            bomb:update(army,1)
            _:expect(countTable(army.bombs)).is(0)
        end)

Now some dots on the Gunner:

function drawGunner()
    pushMatrix()
    pushStyle()
    stroke(255)
    fill(255)
    rectMode(CENTER)
    rect(Gunner.pos.x, Gunner.pos.y, 16,8)
    stroke(255,0,0)
    fill(255,0,0)
    point(Gunner.pos.x, Gunner.pos.y)
    point(Gunner.pos.x-8, Gunner.pos.y)
    point(Gunner.pos.x+8, Gunner.pos.y)
    point(Gunner.pos.x, Gunner.pos.y+4)
    point(Gunner.pos.x, Gunner.pos.y-4)
    popStyle()
    popMatrix()
    Gunner.pos = Gunner.pos + GunMove
end

dots

This surprises me a bit: all the dots seem to be half-on / half-off and it looks pretty symmetric to me. I guess I’ll just stick with the values I have.

Back to the test:

        _:test("Bomb hits gunner", function()
            Gunner = {pos=vec2(50,50)}
            -- gunner's rectangle is 16x8 centered.
            local bomb = Bomb(50,50)
            _:expect(bomb:killsGunner()).is(true)
        end)

Gunner needs to be global, of course. However, this fails:

11: Bomb hits gunner -- Bomb:31: attempt to index a number value (field 'pos')

Oh, duh, the Bomb creation expects a vec2. OK, that works, now let’s move the bomb around to the edges and keep checking:

        _:test("Bomb hits gunner", function()
            Gunner = {pos=vec2(50,50)}
            -- gunner's rectangle is 16x8 centered.
            local bomb = Bomb(vec2(50,50))
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.y = 55
            _:expect(bomb:killsGunner()).is(false)
        end)

I expected that 55 would be out of range and wonder why it shows a hit. Let’s review:

function Bomb:killsGunner()
    -- some of these must be off by one?
    local gunnerTop = Gunner.pos.y + 4
    local bombBottom = self.pos.y - 2
    if bombBottom > gunnerTop then return false end
    -- missile is at the gunner level
    local gunnerLeft = Gunner.pos.x-8
    local bombRight = self.pos.x + 1
    if bombRight < gunnerLeft then return false end
    -- we have hit him or are on his right
    local gunnerRight = Gunner.pos.x+8
    local bombLeft = self.pos.x - 1
    if bombLeft > gunnerRight then return false end
    return true
end

gunnerTop will be 54. Ah. Driver error. Bomb bottom will be 55 +2. I think I’ve mentioned my problems with arithmetic. 😜 OK, this runs:

        _:test("Bomb hits gunner", function()
            Gunner = {pos=vec2(50,50)}
            -- gunner's rectangle is 16x8 centered.
            local bomb = Bomb(vec2(50,50))
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.y = 57
            _:expect(bomb:killsGunner()).is(false)
            bomb.pos.y = 56
            _:expect(bomb:killsGunner()).is(true)
        end)

Let’s do the edges:

        _:test("Bomb hits gunner", function()
            Gunner = {pos=vec2(50,50)}
            -- gunner's rectangle is 16x8 centered.
            local bomb = Bomb(vec2(50,50))
            -- covers x = 42-58
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.y = 57
            _:expect(bomb:killsGunner()).is(false)
            bomb.pos.y = 56
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.x = 41
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.x = 40
            _:expect(bomb:killsGunner()).is(false)
            bomb.pos.x = 59
            _:expect(bomb:killsGunner()).is(true)
            bomb.pos.x = 60
            _:expect(bomb:killsGunner()).is(false)
        end)

I think we’re good on this test. Commit: Bombs kill gunner, tested.

I think I’ll call it done for this morning, maybe do something this afternoon, maybe not. No, wait, let’s do something about the gunner exploding just to ensure we don’t break the game when a missile hits. I’m going to do some hackery, which should be fun.

Exploding the Gunner

Here’s the Gunner setup now:

function setupGunner()
    Gunner = {pos=vec2(112,10)}
    GunMove = vec2(0,0)
end

What shall we have the explosion do? Ah. What if the Gunner had an alive-dead flag and did something in his draw:


function explode()
    Gunner.alive = false
    Gunner.count = 120
end

function setupGunner()
    Gunner = {pos=vec2(112,10),alive=true,count=0,explode=explode}
    GunMove = vec2(0,0)
end

function drawGunner()
    pushMatrix()
    pushStyle()
    if Gunner.alive then
        stroke(255)
        fill(255)
    else
        stroke(255,0,0)
        fill(255,0,0)
        Gunner.count = Gunner.count - 1
        if Gunner.count <= 0 then Gunner.alive = true end
    end
    rectMode(CENTER)
    rect(Gunner.pos.x, Gunner.pos.y, 16,8)
    stroke(255,0,0)
    fill(255,0,0)
    point(Gunner.pos.x, Gunner.pos.y)
    point(Gunner.pos.x-8, Gunner.pos.y)
    point(Gunner.pos.x+8, Gunner.pos.y)
    point(Gunner.pos.x, Gunner.pos.y+4)
    point(Gunner.pos.x, Gunner.pos.y-4)
    popStyle()
    popMatrix()
    Gunner.pos = Gunner.pos + GunMove
end

We define a function explode and we put it into the Gunner table, along with two new values, alive and count. When the Bomb detects a hit, it calls Gunner explode, which sets the alive flag and count. When draw detects the flag it turns the color red, and starts the count down to alive.

I first tried this using self in the explode function. I think that should have worked but certainly using the global does work and it looks like this:

red gunner

OK, let’s sum up.

Summing Up

Despite a morning with some interruptions in it, this went well. We wrote a function to detect bombs colliding with the gunner (a rectangle), and we have that function in roughly the right place in the design.

We tested the function to our satisfaction. We have it set up so that if we change how it works, the tests should still apply, at least until we do something like allow for the non-rectangular shape of the actual player bitmap. At that point maybe we’ll do a kind of bit-by-bit check.

Then, for fun, we just viciously hacked in a way to make the player flash red when it’s hit. That won’t last, but it makes for a better demonstration.

As for learnings, we didn’t make many mistakes, so there weren’t many forehead whacking learnings. We did rediscover how I suck at arithmetic. No news there.

What might make sense, though, would be to devise ways of programming that require me to do less arithmetic. In the present case, some kind of object intersection routine, generalized from the ones we have, might be good. Once we had it done, we could use it without all this + 1 - 4 stuff.

We’re seeing that the Gunner probably wants to be an object. I hammered some behavior into it just for fun, but that’s no way to program in the longer term. There’s even a good chance that it’ll slow me down when I do convert the Gunner to an object, because I glommed up the code today, in a hurry to get a fake explosion.

We’ll see. And I hope to see you next time!

Invaders.zip