I’m wondering how to move these invaders. Let’s do an experiment.

I’ve not read the original source code sufficiently to be sure, but looking at the few videos of the game in play makes it look like the invaders move one row at a time, giving a kind of rippling effect as they march. It’s quite possible that the original screen had enough persistence to allow moving only one row at a time, and that the machine was slow enough to require it.

Be that as it may, we want to replicate that effect. I plan a short experiment this morning to see how that might be done.

Basically I imagine an array of invaders, each with her own coordinates (and other information no doubt). On each Codea draw cycle, we’ll move one row a little bit (and animate them in the real system), and then next cycle do another row, and so on. We’ll probably want to tune the timing, but that can wait.

Since I’ve got some invader sprites defined (thanks again, Dave!), I’ll use one of those. I believe there are 11 invaders across, in five rows. I plan to use scale 4, and to space them on their own width, 16.

Let’s get to it and see what we can work up. A little futzing about gives us this:

-- invadersSpike

function setup()
    invaders = {}
    for row = 0,4 do
        for col = 0,10 do
            table.insert(invaders, vec2(col*16,row*16))
        end
    end
    vader = readImage(asset.documents.Dropbox.inv11)
end

function draw()
    spriteMode(CORNER)
    background(40, 40, 50)
    scale(4)
    for i,at in ipairs(invaders) do
        pushMatrix()
        translate(at.x,at.y)
        sprite(vader,0,0)
        popMatrix()
    end
end

Which draws this:

invaders in lower left corner

Now our “design”, such as it is, has each invader knowing her own coordinates. If they move a row at a time, we could perhaps do something clever, but I’m trying to avoid clever for a while.

I think I’d like to use the middle half of the screen this morning, which I think we can do with a single translate …

function draw()
    spriteMode(CORNER)
    background(40, 40, 50)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        pushMatrix()
        translate(at.x,at.y)
        sprite(vader,0,0)
        popMatrix()
    end
end

Giving this:

invaders centered

Now it seems to me that I should update the array at the beginning of the draw, and then draw it. And almost certainly I shouldn’t do that every time through the draw, 120 times a second on my iPad.

I came up with this:

-- invadersSpike

function setup()
    invaders = {}
    for row = 0,4 do
        for col = 0,10 do
            table.insert(invaders, vec2(col*16,row*16))
        end
    end
    vader = readImage(asset.documents.Dropbox.inv11)
    count = 0
    updateCount = 60
    updateRow = 0
end

function draw()
    update()
    spriteMode(CORNER)
    background(40, 40, 50)
    text(DeltaTime, 100,100)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        pushMatrix()
        translate(at.x,at.y)
        sprite(vader,0,0)
        popMatrix()
    end
end

function update()
    count = (count + 1)%updateCount
    if count == 0 then
        for col = 1,11 do
            invaders[updateRow*11+col] = invaders[updateRow*11+col] + vec2(2,0)
        end
        updateRow = (updateRow + 1)%5
    end
end

Which is nearly good:

marching right

However, there are some clouds on the horizon. I’m displaying DeltaTime there on the lower left, and I’ve seen it drop from 0.008 to 0.016, that is, from 120/second to 60/second. And, of course, most iPads run at 1/60 anyway. So we should probably scale the time and steps to 1/60, but that shouldn’t really affect us today. On the other hand, it’s perhaps best to get used to the 1/60 pace. OK, you talked me into it. Here’s a time-dependent version:

-- invadersSpike

function setup()
    invaders = {}
    for row = 0,4 do
        for col = 0,10 do
            table.insert(invaders, vec2(col*16,row*16))
        end
    end
    vader = readImage(asset.documents.Dropbox.inv11)
    updateRow = 0
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

function draw()
    update(ElapsedTime)
    spriteMode(CORNER)
    background(40, 40, 50)
    text(DeltaTime, 100,100)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        pushMatrix()
        translate(at.x,at.y)
        sprite(vader,0,0)
        popMatrix()
    end
end

function update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        lastElapsedTime = elapsed
        for col = 1,11 do
            invaders[updateRow*11+col] = invaders[updateRow*11+col] + vec2(2,0)
        end
        updateRow = (updateRow + 1)%5
    end
end

Could we get rid of the push/pop by drawing the sprite directly at its location? Turns out we can. This works just fine:

function draw()
    update(ElapsedTime)
    spriteMode(CORNER)
    background(40, 40, 50)
    text(DeltaTime, 100,100)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        sprite(vader,at.x,at.y)
    end
end

What else? Just for fun, let’s make them use both the invader versions, so they’ll “walk”.

We’ll init like this:

function setup()
    invaders = {}
    for row = 0,4 do
        for col = 0,10 do
            table.insert(invaders, {p=vec2(col*16,row*16),v=0})
        end
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2)
    updateRow = 0
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

So now each invader has a position p and a number v, which we’ll use to decide which invader to draw:

function draw()
    update(ElapsedTime)
    spriteMode(CORNER)
    background(40, 40, 50)
    text(DeltaTime, 100,100)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        local v = vaders[at.v+1]
        sprite(v,at.p.x,at.p.y)
    end
end

And we’ll update them this way now:

function update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        lastElapsedTime = elapsed
        for col = 1,11 do
            local vader = invaders[updateRow*11+col]
            vader.p = vader.p + vec2(2,0)
            vader.v = (vader.v + 1)%2
        end
        updateRow = (updateRow + 1)%5
    end
end

And now they change form as they walk across the screen:

invaders walk

That’ll do for now, let’s lift our head up and see what we’ve learned from this little experiment.

Summing Up

Row and column two different ways

I noticed that I did the row/column calculations two different ways, starting at 0 during init and at 1 during draw. This is a result of Lua starting arrays at 1, which I usually like, but it means that we have to add or subtract one sometimes to get coordinates to be right. We should pick one way or the other, I think. Clarity is more important to me than saving an add.

Row/col or x/y?

I also noticed that I used “row” and “col” but of course the coordinates are “x” and “y”. We should normalize that, by using x and y, since that’s how vectors are represented in Lua.

Things inline

If you’ve looked at my work at all, for example in the Asteroids series, you know that I favor putting things into objects. I’d very likely make each invader her own little object, and tell each one to draw herself, and so on.

Taking a leaf from Mary Rose Cook’s simple invaders example, and Dave1707’s simple conversion of the bitmaps, I’m going to try keeping everything in Main rather longer than I normally would.

I really think that my practice is better if I’m going for a program of more than very low complexity, but let’s go the other way and see what happens. We’ll either refactor when the cognitive load gets too high, or, if it never does, just to see the difference.

Limits

The current example program will march the invaders right over the edge. We’ll want to find a simple way to decide which way they’re moving, and when to reverse. And, of course, when they reverse, they also move a step downward.

This will come down to a very simple comparison and a change of sign somewhere, but knowing me, I’ll get the math wrong a time or two.

Testing

That reminds me of testing. If we had an object representing Susie, our canonical invader, we could teach her to step, and she’d step right until it was time to stop, step down and start moving left until it was time to stop, and so on. We could write tests for Susie’s behavior.

Or we might decide to do stepping at a row basis, and have a row object. Who knows: whatever we made, it would have stepping logic nicely broken out.

The way this code is shaping up, it’ll be hard to test, because there are no functions, much less objects, there to test. We are of course comfortable with just looking at the screen to see if it works … but that’s not as comfortable, for me at least, as having some solid tests.

We’ll be staring this issue right in its cold dead space invader eyes as we go forward. We will triumph, I’m sure of that. But it could be a messy battle. We’ll see.

See you next time!


The Code:

-- invadersSpike

function setup()
    invaders = {}
    for row = 0,4 do
        for col = 0,10 do
            table.insert(invaders, {p=vec2(col*16,row*16),v=0})
        end
    end
    vader1 = readImage(asset.documents.Dropbox.inv11)
    vader2 = readImage(asset.documents.Dropbox.inv12)
    vaders = {vader1,vader2}
    updateRow = 0
    lastElapsedTime = ElapsedTime
    timeToUpdate = 10/60
end

function draw()
    update(ElapsedTime)
    spriteMode(CORNER)
    background(40, 40, 50)
    text(DeltaTime, 100,100)
    translate(WIDTH/4, HEIGHT/2)
    scale(4)
    for i,at in ipairs(invaders) do
        local v = vaders[at.v+1]
        sprite(v,at.p.x,at.p.y)
    end
end

function update(elapsed)
    if elapsed >= lastElapsedTime + timeToUpdate then
        lastElapsedTime = elapsed
        for col = 1,11 do
            local vader = invaders[updateRow*11+col]
            vader.p = vader.p + vec2(2,0)
            vader.v = (vader.v + 1)%2
        end
        updateRow = (updateRow + 1)%5
    end
end