Learnings from the ancient scrolls. What shall we do about them?

I’ve been reading the assembly code for the original Space Invaders (more accurately, mostly reading the excellent comments provided by Topher Cantrell of computerarcheology.com) and I’ve learned that the original game draws exactly one invader every 60th of a second. Presently our program is drawing one row at a time, every 10 60ths. Not the same thing at all.

Our mission today will be to change the system to move one invader at a time, one every 60th. We can’t really draw them one at a time, because of how Codea works. Let me explain../ No, there is too much. Let me sum up.

The original game had a raster-scan display that displayed an image of what was in memory at a standard location. If a bit in that area was 1, there would be a dot on the screen. If the bit was z0, there would be no dot.

Codea draws an entirely new screen every 120th, or 60th, or sometimes even 30th of a second. The thing you do is draw everything, every time. So we’re displaying all 55 invaders, or however many are left alive, every time around.

In the original version, if the program erased the bits for one invader, and drew her in another nearby location (and perhaps with the other bitmap) that invader would appear to move, by the magic of how our vision works. In our version, we can move them one at a time, but we will have to draw them all.

There is another way, involving not fully clearing the screen and letting it “decay” like an old-fashioned CRT, but I don’t think we want to try that. Although, having said it, I’m sure we’ll at least try it to see what happens. Anyway, for now, we’ll move one, and draw them all.

Existing Code

In Main, we do this:

function draw()
    Rank:update(ElapsedTime)
...
    Rank:drawInvaders()
end

The … represents some screen setup that isn’t of interest just now. In Rank:update, we have this:

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

This code checks to see if it is time to update, checks to see if we need to reverse, and then moves one row (updateRow+1) in the proper direction. Each row is represented by an instance of Rank.

For what we need to do now, I think we want all the invaders in a single Rank instance. (Which will make the name undesirable but let’s worry about that in a moment.) First let’s see if we can get down to a single rank on construction. I’ll comment out the update entirely.

The Rank instances are created with this:

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

Now that we’ll do all five at once, let’s do this instead:

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

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

function Rank:createRanks()
    Ranks = { Rank(0) }
end

My hope is that I can just return the single Rank in the table that we use in the update and draw now, and fix up Main later. Drawing will surely not work well now. It looks like this:

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

Somewhat to my surprise, this draws all the invaders. They all use the same bitmap, which we’ll try to fix today.

all invaders

Let’s see what’s going on in update. This might not be too hard.

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

Well, we’re referring to updateRow, which is a global in Main. (The woes of working on a Spike. Keep this in mind, it is this kind of thing that makes me recommend against going forward with spike code.)

Since there’s just one Rank now, let’s let him have a member variable invaderNumber:

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

And in update, we have some work to do. I’ll leave the reverser code there. Either it works or we’ll fix it. But we need to update just one individual at a time. I’ll erase the update code and put in what I intend.

Ah. This doesn’t quite work, because Main is calling the class method Rank:update, not the instance method. We’ll have to fix Main.

-- invadersSpike

local MyRank

function setup()
    MyRank = Rank()
    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 = 1/60
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:drawInvaders()
end

This nearly works now, but it doesn’t. Here’s a video:

not quite right

The bottom row isn’t stepping down at all, and the upper rows are working oddly as well, including one invader who just can’t seem to keep up with the others.

Here again we see the difficulties that come from working with a Spike, or with any code that isn’t decently factored. That said, this is a pretty odd change, from going row by row to item by item, and we’re probably not far off.

Even treating this as a spike is OK. I am, after all, trying to work out how this should be done in my mind, letting the code participate in my thinking.

I’m calling a lunch break: I got a late start today,.

After Lunch

We now have a brief period of lucidity before it’s time for a siesta or some reading.

I’m inclined to think of what’s going on as follows:

I’d really like to have the bottom-most left-most alien serve as the key locator, but the updated position of each separate invader needs to change only when that invader’s turn comes in the update loop. In the original program, you could move the key invader, and since others were displayed in their old position until rewritten, the rippling worked as desired.

We could, of course, do away with the rippling. But it’s kind of neat and I’d rather retain it.

But if we’re not going to use that offset for anything, we should consider getting rid of it. It might have value as the rows move lower and lower when new waves come in, but I’m not seeing much value there right now.

As the ranks move, the normal state of affairs is that some are moved and some not. It seems that representing their actual location in the individual invader makes sense. I’d like to make them immutable, but at this moment I don’t see how, unless we moved them from an “undone” collection to a “done” collection. Hm, would that be interesting?

Let’s try that. It’s sure to be interesting, even if it’s a bad idea.

This got slightly interesting because I had to move the reversing check from draw down to update (and it’s still not really correct, which I’ll discuss below. After a bit of cleaning up, we have this:

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 -- allmoved
        self:copyToUndone()
        self.updateStep.y = 0
        if self.updateStep.x > 0 and self.invaders[55].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

This is of course all jammed together but it seems to do the right thing, The 55 aliens seem to step at about one second for all five ranks, which is about one alien per 60th of a second as desired.

all-good

This is a decent wrapping point for today. Let’s sum up.

Summing Up

I said I’d mention reversal checking. An interesting sidelight is that the original program decides whether it is time to reverse by searching all the bytes down either the rightmost column of the screen where invaders should reverse, or the left column where they should reverse. If it finds a 1 byte, that means an invader has been drawn in that column, and therefore it is time to reverse. Basically it’s a very primitive ray scan. We don’t have that luxury. Right now, invaders can’t die, so I’m just checking the first one and the last one, but we’ll need to check the leftmost and rightmost in the real game. This will do for now, but it won’t do.

Today’s development felt very much to me like hammering on it until it worked, not very much like thinking in small steps toward working and then building in small steps, always working. I think there are a few reasons why it felt that way.

First, we’re trying to make throwaway code not be thrown away. Since it was not written to be clean, and we haven’t done everything we could have to make it clean, the dirty bits are probably making us stumble.

Second, we were definitely changing a fundamental notion in the design, from displaying by rows to displaying individually, and that was sure to cause us some trouble. And we didn’t actually do that by throwing the one away and doing the other, see above, though in my final spurt I pretty much finally tossed all of update and redid it.

Third, because it was all sort of working, I was changing more than one thing at a time, typically the single stepping plus the updating of the step.

Fourth, there are bits and pieces of wrongness still in there that cause mental friction. One such thing is the member variable offset, which is initialized and never used.

I just deleted that now.

One more cleanup thing. There’s no reason for Main to call the drawInvaders method. We should rename it to draw, and call that. I’ll do that as well.

Summing up the summing up, we’ve got invaders displaying one by one, as desired, and they are reversing correctly, although they’re not making that decision quite as cleverly as they might.

We could continue to move this code forward, or declare victory and move to a new clean sheet of paper. We’ll decide that next time. I’m inclined right now to stick with the dirty paper, because I think we’ll learn more about how not to do things, which is an important bit of learning.

See you next time!


The Code

InvadersSpike.zip