Let’s see what happens if we try to refactor our little spike into something reasonable.

OK, I’m freshly showered and shaved, and back from a nearby Starbucks with an iced chai. My hair is mussed from the convertible ride and I am ready to rumble. My plan, just for fun, is to see how easy or hard it is to turn the current little spike into decent code. Things like this are about as bad as my code ever gets without trying to write bad code, so this should be a decent experiment, albeit small.

Here’s the whole thing:

-- invadersSpike

function setup()
    ranks = {}
    for rank = 0,4 do
        local rankTable = {}
        rankTable.rank = rank
        rankTable.invaders={}
        rankTable.offset = vec2(0,0)
        table.insert(ranks,rankTable)
        for col = 0,10 do
            table.insert(rankTable.invaders, {p=vec2(col*16,rank*16)})
        end
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

function draw()
    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)
    drawInvaders()
end

function drawInvaders()
    scale(4)
    for r,rankEntry in ipairs(ranks) do
        for i,invader in ipairs(rankEntry.invaders) do
            local pos = invader.p + rankEntry.offset
            if updateStep.x > 0 and pos.x > 224 - 16 then
                reverse = true
            elseif updateStep.x < 0 and pos.x < 0 then
                reverse = true
            end
            local v = vader1
            sprite(v, pos.x, pos.y)
        end
    end
end

function update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        if updateRow == 0 and reverse then
            updateStep = - updateStep - vec2(0,2)
            reverse = false
        end
        lastElapsedTime = elapsed
        ranks[updateRow+1].offset = ranks[updateRow+1].offset + updateStep
        updateRow = (updateRow + 1)%5
        if updateRow == 0 then updateStep.y = 0 end
    end
end

You see how it is, even in a spike, I can’t resist breaking things out, like the update function and the drawInvaders function. I do that habitually, because it helps me when the code expresses my thoughts, and when I write something like that out longhand, and discover a bit later what’s going on, I often break it out. I find it a useful habit to have developed.

But now let’s see what we can really see in this thing. Here’s setup on its own:

function setup()
    ranks = {}
    for rank = 0,4 do
        local rankTable = {}
        rankTable.rank = rank
        rankTable.invaders={}
        rankTable.offset = vec2(0,0)
        table.insert(ranks,rankTable)
        for col = 0,10 do
            table.insert(rankTable.invaders, {p=vec2(col*16,rank*16)})
        end
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

It starts out by building a table called ranks, and in fact each row of the table represents a rank of invaders. Each table entry has the rank (0-4), an offset which is always (0,0), which doesn’t seem very useful, and a table called invaders, containing 11 tables, each of which represents an invader. Those invader tables each have one member, p, which is the invader’s column number in x and row number in y, both multiplied by 16, the width of an invader image.

A glance at the drawInvaders code tells us that the rank offset is added to the column/row info of each invader to draw her, and in update we see that we step the rank offsets left and right and down as need be, to move the rank. Each rank has its own offset, because they step downward separately to give that neat rippling effect.

So it seems like rank is a good concept to break out, and perhaps also invader. Both are already represented by tables of data, but since they aren’t objects, their behavior is threaded around in the general procedures in Main. We would hope that making them full-fledged objects would improve the overall cohesion of the code.

Let’s begin with a Rank class. We do this by rote, just giving it the same elements as the table we’re now using:

Rank = class()

function Rank:init(rank, offset, invaders)
    self.rank = rank
    self.offset = offset
    self.invaders = invaders
end

function Rank:draw()
end

Now we plug it into setup:

function setup()
    ranks = {}
    for rank = 0,4 do
        local rankItem = Rank(rank,vec2(0,0),{})
        table.insert(ranks,rankItem)
        for col = 0,10 do
            table.insert(rankItem.invaders, {p=vec2(col*16,rank*16)})
        end
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

And it’s all good. Everything works, which shouldn’t be a surprise: there’s really no difference between a table and an object unless and until we put behavior on the object.

We notice that the second and third inputs to Rank are constant, so we move them inside:

function Rank:init(rank, offset, invaders)
    self.rank = rank
    self.offset = vec2(0,0)
    self.invaders = {}
end

function setup()
    ranks = {}
    for rank = 0,4 do
        local rankItem = Rank(rank)
        table.insert(ranks,rankItem)
        for col = 0,10 do
            table.insert(rankItem.invaders, {p=vec2(col*16,rank*16)})
        end
    end
...

It’s a bit weird for our init to be pushing invader table entries into the rank object’s table. Let’s make him do that. He has enough information to initialize the invader table himself:

function Rank:init(rank, offset, invaders)
    self.rank = rank
    self.offset = vec2(0,0)
    self.invaders = {}
    self:initInvaders()
end

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

That means we can delete that loop from the setup:

function setup()
    ranks = {}
    for rank = 0,4 do
        local rankItem = Rank(rank)
        table.insert(ranks,rankItem)
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

Now the two lines in the loop can be turned into one:

function setup()
    ranks = {}
    for rank = 0,4 do
        table.insert(ranks,Rank(rank))
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

Everything still works fine. We could break out that little patch of code at the top. What if we made a class method on Rank to produce a rank table?

function Rank:createRanks()
    local ranks = {}
    for rank = 0,4 do
        table.insert(ranks,Rank(rank))
    end
    return ranks
end


function setup()
    ranks = Rank:createRanks()
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
...

We could even do this, which is arguably more clear:

function Rank:createRanks()
    return { Rank(0), Rank(1), Rank(2), Rank(3), Rank(4) }
end

I rather like that.

I’d commit this code if my code manager were working. As it is, I’ll just nervously proceed, remembering that if the going gets tough, the source is all here in the articles. Arrgh.

What about drawing?

Drawing

The draw code looks like this:

function drawInvaders()
    scale(4)
    for r,rankEntry in ipairs(ranks) do
        for i,invader in ipairs(rankEntry.invaders) do
            local pos = invader.p + rankEntry.offset
            if updateStep.x > 0 and pos.x > 224 - 16 then
                reverse = true
            elseif updateStep.x < 0 and pos.x < 0 then
                reverse = true
            end
            local v = vader1
            sprite(v, pos.x, pos.y)
        end
    end
end

Now what I’d really like to have right now is a smart table for the ranks, with a draw method on it. But for now, let’s just move this functionality over to the Rank object, who at least has the keys to the kingdom.

function Rank:drawInvaders(ranks)
    scale(4)
    for r,rankEntry in ipairs(ranks) do
        for i,invader in ipairs(rankEntry.invaders) do
            local pos = invader.p + rankEntry.offset
            if updateStep.x > 0 and pos.x > 224 - 16 then
                reverse = true
            elseif updateStep.x < 0 and pos.x < 0 then
                reverse = true
            end
            local v = vader1
            sprite(v, pos.x, pos.y)
        end
    end
end

This is called in Main draw:

function draw()
    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)
    Rank:drawInvaders(ranks)
end

We note that in setup and draw, there’s no use for the ranks table, which could be internal to Rank. But there’s update:

function update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        if updateRow == 0 and reverse then
            updateStep = - updateStep - vec2(0,2)
            reverse = false
        end
        lastElapsedTime = elapsed
        ranks[updateRow+1].offset = ranks[updateRow+1].offset + updateStep
        updateRow = (updateRow + 1)%5
        if updateRow == 0 then updateStep.y = 0 end
    end
end

This too could be put into Rank. Its timing stuff is about when to draw a Rank, which is the rank’s business, as is the reversing logic. We’ll move it wholesale, passing in the ranks table to make it work:

function Rank:update(ranks,elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        if updateRow == 0 and reverse then
            updateStep = - updateStep - vec2(0,2)
            reverse = false
        end
        lastElapsedTime = elapsed
        ranks[updateRow+1].offset = ranks[updateRow+1].offset + updateStep
        updateRow = (updateRow + 1)%5
        if updateRow == 0 then updateStep.y = 0 end
    end
end


function draw()
    Rank:update(ranks, 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)
    Rank:drawInvaders(ranks)
end

Now there is no reason for Main to know about the ranks table at all. We’ll make them local to Rank:

Rank = class()

local Ranks;

function Rank:createRanks()
    Ranks = { Rank(0), Rank(1), Rank(2), Rank(3), Rank(4) }
end

I capitalized Ranks for two reasons. First of all, it’s not a local variable inside each function, it’s local to the class, and therefore global to those functions, while unknown in other tabs. I like globalish variables to be capitalized.

Second, by capitalizing it, I can more easily detect references that need fixing, since they’ll all be nil. (Yes, I know, if my compiler were more strict, it would tell me. It is what it is, and anyway I like it this way.)

I like what we’ve got here. We’ve created one class, and moved everything pertaining to it into another tab, leaving Main tab more concerned with overview matters, and everything from ranks on down encapsulated in the Rank tab. Let’s sum up and call it a morning.

Summing Up

While it is generally a very good idea to throw spikes away, as an experiment, we’ve quickly refactored this rather messy code into far less messy code, that is beginning to take on the shape of a decent design. It’s easy to see that inside Rank, there is an Invader class trying to get out, and we’ll probably do that next time.

There’s still plenty of weird stuff going on, such as the edge detection and reverse handling, but it’s mostly separated out a bit. The Main tab is concerned, in setup, with the global situation of invaders and graphical objects, which will need to be enlarged somehow. And we’ll surely have to push the invader graphics down into the invaders in some fashion.

The draw logic in Main is just drawing the background and my timing display, so it i entirely ad hoc now and will someday be replaced with whatever the background turns out to be.

For now, I think we’ve done a good thing. Tweet me if you agree, or don’t.

See you next time!

Code

-- invadersSpike

function setup()
    Rank:createRanks()
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    updateStep = vec2(2,0)
    reverse = false
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

function draw()
    Rank: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)
    Rank:drawInvaders()
end

Rank = class()

local Ranks;

function Rank:init(rank, offset, invaders)
    self.rank = rank
    self.offset = vec2(0,0)
    self.invaders = {}
    self:initInvaders()
end

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

function Rank:createRanks()
    Ranks = { Rank(0), Rank(1), Rank(2), Rank(3), Rank(4) }
end

function Rank:drawInvaders()
    scale(4)
    for r,rankEntry in ipairs(Ranks) do
        for i,invader in ipairs(rankEntry.invaders) do
            local pos = invader.p + rankEntry.offset
            if updateStep.x > 0 and pos.x > 224 - 16 then
                reverse = true
            elseif updateStep.x < 0 and pos.x < 0 then
                reverse = true
            end
            local v = vader1
            sprite(v, pos.x, pos.y)
        end
    end
end

function Rank:update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        if updateRow == 0 and reverse then
            updateStep = - updateStep - vec2(0,2)
            reverse = false
        end
        lastElapsedTime = elapsed
        Ranks[updateRow+1].offset = Ranks[updateRow+1].offset + updateStep
        updateRow = (updateRow + 1)%5
        if updateRow == 0 then updateStep.y = 0 end
    end
end

function Rank:draw()
end