An experiment and some planning. Then even some doing.

The original invaders game sped up as the game went on, not so much because of special code, but because drawing fewer invaders took less time, and therefore they progressed more rapidly. We have a similar situation in our update method: we move another step when we run out of invaders to update.

This morning, for entertainment, I want to observe how they move when there are fewer of them.

one invader

It does speed up, but at a glance I’d say it’s not as much as the change we see in videos of the original game. I’m not sure what would make the difference, although I do recall that when it’s down to just one invader, the original game steps two pixels left per cycle, but three to the right, to make her harder to hit.

Anyway, that was interesting. (It doesn’t take much to entertain me in these perilous times.)

Down To It

We need to get down to it here, deciding what the point of the exercise is and how best to proceed.

The main point is to have a bit of fun coding something up. That’s why I’m doing it. To be fun, it has to provide interesting problems and some learning.

I like sharing what I’m thinking as I program, and I know of at least three people who enjoy being on the receiving end. I think they enjoy seeing me get into trouble due to doing something they’d never have done.

I like showing that we can turn poor code into better code without rewriting it and without long delays between releasing useful features. I have two reasons for liking it. A, it feels good to make something better. B, of all my many failures in my programming life, the most common has been failure to deliver something in time.

How do those things apply to where we are today, with Space Invaders?

We’d better start showing more progress toward a playable game. We have been working on a spike, or a series of them, to work out how to move and display these invaders, how to represent the data, and so on. We’ve learned some things: how about putting them into practice?

OK, we’ll focus on something more like a game. Shall we start over, or work from where we are?

The rules of spikes say that it’s always best to start over. The realities of coding say that our code gets worse and worse, even when we try to keep it looking good. Therefore, we won’t start over, because our code is still closer to worse than it is to better. That will give us a chance to work with “legacy” code and to try to improve it.

OK, we have a direction. Now for a plan.

A Cunning Plan

I don’t expect an actual plan out of this planning but we’ll at least have a list of things to do, and perhaps an order we might do them in. Candidates include:

  • Make the invaders do their shape-shifting thing
  • Display the shields
  • Display the player
  • Move the player
  • Fire missiles from player at invaders
  • Make missiles kill invaders
  • Fire missiles from invaders at player
  • Make missiles slowly destroy shields
  • Make missiles kill player
  • Implement the saucer
  • Do scoring

Wow, there’s a lot, isn’t there? What’s first?

I think to keep our customer satisfied, we should make the invaders display their two shapes, to give a bit more variation to their marching and to make them look generally more menacing.

Invader Shapes

We don’t have an object representing invaders, just a table:

            table.insert(self.invaders, {p=vec2(col*16,rank*16)})

Each invader entry just has a member p, consisting of her position relative to the left-bottom invader. We need to give each invader her two shapes, and then enable her to decide which shape to display. Right now, we have just read in two shapes, and we’re only using one of them:

function setup()
    MyRank = Rank()
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
end

Let’s begin by just passing those two into the rank creation, and extend the list later:

function setup()
    local vader1 = readImage(asset.documents.Dropbox.inv11)
    local vader2 = readImage(asset.documents.Dropbox.inv12)
    local vaders = {vader1,vader2}
    MyRank = Rank(vaders)
end

In Rank, I’ll just give everyone those two for now:

function Rank:init(images)
    self.invaderNumber = 1
    self.updateStep = vec2(2,0)
    self.invaders = {}
    self.undone = {}
    self.reverse = false
    self:initInvaders(images)
    self.lastElapsedTime = ElapsedTime
    self.timeToUpdate = 1/60
end

function Rank:initInvaders(images)
    for rank = 0,4 do
        for col = 0,10 do
            table.insert(self.invaders, {p=vec2(col*16,rank*16), images=images})
        end
    end
end

Now we need a toggle to decide which image to draw. Since table indexes start at 1, it would be nice if the image toggle went 1,2,1,2. It should change every time we compute a new step in update:

function Rank:update(elapsed)
    if elapsed < self.lastElapsedTime + self.timeToUpdate then return end
    if #self.undone ~= 0 then
        local invader = self.undone[1]
        table.remove(self.undone, 1) -- costly
        invader.p =invader.p + self.updateStep
    else -- all moved
        self:copyToUndone()
        self.updateStep.y = 0
        if self.updateStep.x > 0 and self.invaders[#self.invaders].p.x > 224 - 16 then
            self.reverse = true
        elseif self.updateStep.x < 0 and self.invaders[1].p.x < 0 then
            self.reverse = true
        end
        if self.reverse then
            self.updateStep = - self.updateStep - vec2(0,2)
            self.reverse = false
        end
    end
    self.lastElapsedTime = elapsed
end

Wow, if ever there was legacy code, this is it. We must improve that, but not now. I think the toggle goes there under the “all moved” comment. And it needs to get initialized in init and it should be a member variable in Rank.

function Rank:update(elapsed)
    if elapsed < self.lastElapsedTime + self.timeToUpdate then return end
    if #self.undone ~= 0 then
        local invader = self.undone[1]
        table.remove(self.undone, 1) -- costly
        invader.p =invader.p + self.updateStep
    else -- all moved
        self.imageToggle = self.imageToggle > 2 and 1 or self.imageToggle + 1
        self:copyToUndone()
	...
    end
    self.lastElapsedTime = elapsed
end

And then to draw:

function Rank:draw()
    pushMatrix()
    pushStyle()
    scale(4)
    rankEntry = self
    for i,invader in ipairs(rankEntry.invaders) do
        local pos = invader.p
        local v = invader.images[self.imageToggle]
        sprite(v, pos.x, pos.y)
    end
    popStyle()
    popMatrix()
end

When we run that we discover that the tricky code for updating imageToggle was too tricky for us. It should be:

        self.imageToggle = self.imageToggle == 2 and 1 or self.imageToggle + 1

There’s some way to write that with modulus but this is easier in my view. YMMV.

Now they all have the same form, but they do switch between the two walking poses:

It remains to give the invaders their correct forms, based on their row.

function setup()
    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 = {vader11,vader12, vader21, vader22, vader31,vader32}
    MyRank = Rank(vaders)
end

Now they’re all in there. What’s the order in the rows? Row 0 and 1 get invader1, 2 and 3 get number 2, and 4 gets number 3. Kind of messy but for now we’ll just bang it in this way:

local invaderOffset = {0,0, 2,2, 4}

function Rank:draw()
    pushMatrix()
    pushStyle()
    scale(4)
    rankEntry = self
    for i,invader in ipairs(rankEntry.invaders) do
        local pos = invader.p
        local imageBase = invaderOffset[1 + pos.y//16]
        local v = invader.images[self.imageToggle + imageBase]
        sprite(v, pos.x, pos.y)
    end
    popStyle()
    popMatrix()
end

That works as intended:

different invaders

Well, almost. It blows up when it changes direction, saying that imageBase is nil. Which means we got a weird number in that subscript calculation. I’m not sure why.

And I don’t care, because having seen that code, I don’t want to initialize all the invaders to have all the images. I want each invader to have her own. I expect with that change, this problem will go away.

local invaderOffset = {1,1, 3,3, 5}

function Rank:initInvaders(images)
    for rank = 0,4 do
        for col = 0,10 do
            local imageBase = invaderOffset[1 + rank]
            local herImages = {images[imageBase], images[imageBase+1]}
            table.insert(self.invaders, {p=vec2(col*16,rank*16), images=herImages})
        end
    end
end

function Rank:draw()
    pushMatrix()
    pushStyle()
    scale(4)
    rankEntry = self
    for i,invader in ipairs(rankEntry.invaders) do
        local pos = invader.p
        local v = invader.images[self.imageToggle]
        sprite(v, pos.x, pos.y)
    end
    popStyle()
    popMatrix()
end

And yes, it does go away. There’s a lesson here.

good invaders

I could have debugged that code, and it was really tempting to do so, because I wondered what I had done wrong. I still wonder. But since I now saw that the approach was wrong, the fact that I missed some detail was and is irrelevant. The new scheme works fine and is more efficient. Each invader just has her own two images, and they’re applied as needed.

My morning chai is getting diluted. That happened yesterday as well. Maybe I should get a Grande instead of a Venti. But it’s a shame to drive the 5 miles to the store just for such a small drink.

But I digress. I think we’re at a good stopping point, since we have our invaders happily dancing along, and the code isn’t much worse than when we started. We really do need some cleanup, but I think we’ve got to show some feature progress the next day or two,

Let’s sum up.

Summing Up

Did you think it was a bit odd to go to the trouble of storing the image table in each invader table, and then using it in Rank:draw? We could have kept it in Rank and used it there.

My view is that the images belong to the invaders, not to the overall collection of invaders. And of course I have in mind converting them to objects if the time seems right to do so. So I gave each invader her own images.

Equally odd was the fact that I did enough weird image calculation in Rank:draw to cause a mistake. Even without the mistake, half that calculation didn’t belong there.

Why? Duplication. Not the two lines of code looking the same duplication, but duplication of the execution of the calculation for every drawing of every invader. We improved the code by moving the initialization of the invader images up into the initialization code.

Duplication comes in many forms, including:

  • Exact textual duplication such as with cut and paste;
  • Near textual duplication with a few local mods;
  • Very similar code lying about;
  • Code doing the same thing different ways;
  • Code being executed more times than it should be;

Of these, of course the first couple are the most obvious, but all forms of duplication are hints that the code could be better.

But why didn’t I do the images in the initialization right off the bat? Because my eye was on the wrong ball: I was thinking about how to draw them, so that was where I did the work. It was only after raising my head up that I realized I was looking in the wrong place for that function.

This is why small steps are important. They give us more opportunity to look up, and when we do discover that there’s a better idea, we haven’t so much sunk cost. That makes it easier to back out the not so good thing and put in the somewhat better thing.

If we put in enough somewhat better things, the code can turn out to be really quite nice.

Anyway, enough for today. I’m not sure if I’ll stick with Invaders tomorrow or not. I do have some things that need doing and saying about Asteroids as well.

I guess we’ll have to wait and see. See you then!


The Code

-- invadersSpike

local MyRank

function setup()
    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 = {vader11,vader12, vader21, vader22, vader31,vader32}
    MyRank = Rank(vaders)
end

function draw()
    MyRank:update(ElapsedTime)
    background(40, 40, 50)
    rectMode(CENTER)
    noFill()
    stroke(255)
    strokeWidth(2)
    rect(WIDTH/2, HEIGHT/2, 224*4, 256*4)
    spriteMode(CORNER)
    text(DeltaTime, 100,100)
    translate(WIDTH/2-224*2, HEIGHT/2)
    MyRank:draw()
end

Rank = class()

local Ranks;

function Rank:init(images)
    self.invaderNumber = 1
    self.updateStep = vec2(2,0)
    self.invaders = {}
    self.undone = {}
    self.reverse = false
    self:initInvaders(images)
    self.lastElapsedTime = ElapsedTime
    self.timeToUpdate = 1/60
    self.imageToggle = 1
end

local invaderOffset = {1,1, 3,3, 5}

function Rank:initInvaders(images)
    for rank = 0,4 do
        for col = 0,10 do
            local imageBase = invaderOffset[1 + rank]
            local herImages = {images[imageBase], images[imageBase+1]}
            table.insert(self.invaders, {p=vec2(col*16,rank*16), images=herImages})
        end
    end
end

function Rank:draw()
    pushMatrix()
    pushStyle()
    scale(4)
    rankEntry = self
    for i,invader in ipairs(rankEntry.invaders) do
        local pos = invader.p
        local v = invader.images[self.imageToggle]
        sprite(v, pos.x, pos.y)
    end
    popStyle()
    popMatrix()
end

function Rank:update(elapsed)
    if elapsed < self.lastElapsedTime + self.timeToUpdate then return end
    if #self.undone ~= 0 then
        local invader = self.undone[1]
        table.remove(self.undone, 1) -- costly
        invader.p =invader.p + self.updateStep
    else -- all moved
        self.imageToggle = self.imageToggle == 2 and 1 or self.imageToggle + 1
        self:copyToUndone()
        self.updateStep.y = 0
        if self.updateStep.x > 0 and self.invaders[#self.invaders].p.x > 224 - 16 then
            self.reverse = true
        elseif self.updateStep.x < 0 and self.invaders[1].p.x < 0 then
            self.reverse = true
        end
        if self.reverse then
            self.updateStep = - self.updateStep - vec2(0,2)
            self.reverse = false
        end
    end
    self.lastElapsedTime = elapsed
end

function Rank:copyToUndone()
    self.undone = table.pack(table.unpack(self.invaders))
end