Space Invaders 4
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:
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:
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:
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:
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