Space Invaders 16: Marching Orders
I think today I’ll work on making the invaders march back and forth. Right now they just march forth and forth and forth …
OK, well, that’s the plan. Let’s see what they do now:
function Army:update()
-- if itIsTimeToUpdate() then
-- resetTimeToUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(motion)
end
-- end
end
It begins to come back to me. Each time we draw, we call update
at the end of the draw function. We want to update only every 60th of a second, and have yet to write that code (in this version). If it’s time to update, we move the next invader, who will return false if she is not dead and true if she is dead. This makes the cycle run faster as more and more invaders have um gone to their maker. And it gives the invader motion a nice rippling effect, just like the old game, which couldn’t afford to update more than one at a time.
Setting aside the clock, we need to add the following functionality:
- If any invader is at the edge of the screen, we want to reverse their direction, including a step downward.
- If the last invader has been moved down, we want to continue the side-to-side motion without the downward part.
There will be more, having to do with getting to the bottom, but this is the primary behavior we want.
So, sketching our intention, it might look like this:
Oops! Looking at the code and thinking about it, I realize that the rule needs to be:
- If the last invader has been moved, and any invader is at the edge of the screen, we want to reverse their direction, including a step downward.
So both our operations are contingent on the last invader having been moved. We have that logic embedded in nextInvader
:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
end
local inv = self.invaders[self.invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
Well, we have a place to stand, there in that if
statement, so let’s put our new stuff in there and then see if we like it. Make it work, then make it right.
This is roughly what we’d like to say:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
if self.overTheEdge then
motion = motion*-1 - vec2(2,0)
self.overTheEdge = false
else
motion.y = 0
end
self.invaderNumber = 1
end
local inv = self.invaders[self.invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
I plan to stub out the timing stuff for now, but preferred to uncomment it so that the code looked more like real.
I note in passing that motion is a global. It shouldn’t be. Probably it should be an Army property. Right now I can’t fix it, because I’m on a big red bar: this code won’t run.
Another issue is the flag overTheEdge
, which needs to be a property of the Army. How will it get set? Each live invader should ask itself if it is over the edge, and if it is, set the Army flag true, leaving it alone otherwise. That way, when we reach the end of the cycle, if anyone has reached the limit, we’ll remember it.
It seems to me that that check should be in Invader:update
, which looks like this:
function Invader:update(motion)
if self.alive then
self.pos = self.pos + motion
return false
else
return true
end
end
But an invader has no lines of communication to the Army. We’ll provide that:
function Invader:update(motion, army)
if self.alive then
self.pos = self.pos + motion
if self:overEdge() then
army:iAmOverEdge()
end
return false
else
return true
end
end
I quickly rethought that while implementing iAmOverEdge
as:
function Invader:update(motion, army)
if self.alive then
self.pos = self.pos + motion
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
function Army:invaderOverEdge(invader)
self.overTheEdge = true
end
We have no use presently for knowing specifically who is over the edge, but it makes more sense to me to pass the info back anyway.
This would almost work if the invaders knew how to know they are over the edge. I think they move from 0 to 244, don’t they? Let’s try that.
function Invader:overEdge()
return self.pos.x >= 244 or self.pos.x <= 0
end
Ah. You should have seen me fretting over whether it should be >=
or >
, <=
or <
. It comes down to whether we check the value before or after moving. We really should write a test for this to say what the rules are. Let me try.
_:test("over right edge", function()
local army = Army()
local testInvader = army.invaders[#army.invaders]
testInvader.pos.x = 240
army:update()
_:expect(army.overTheEdge).is(false)
end)
This first failed because that clock checking needed to be calls to self:
function Army:update()
if self:itIsTimeToUpdate() then
self:resetTimeToUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(motion, self)
end
end
end
And I stubbed those routines. We need to return to that, there’s a testing lesson to be learned. But we’re on a quest just now and can’t be diverted. The test fails:
9: over right edge -- Invader:16: bad argument #-1 to '__add' (vec2)
Here’s that code:
function Invader:update(motion, army)
if self.alive then
self.pos = self.pos + motion -- <--- line 16
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
else
return true
end
end
is it pos
that is bad, or motion
? motion
is passed in as a global (still) so it is probably OK. Somehow it must be ‘pos`.
A quick insertion of an assert proves me wrong. The value of motion
is nil in my test. (Meanwhile the invaders are moving along just fine on the screen. Remind me to find a way to switch them off until after the tests are over.)
OK, so how did this happen? Ah! It’s in setup. Arrgh:
function setup()
runTests()
--if true then return end
TheArmy = Army()
setupGunner()
Missile = {v=0, p=vec2(0,0)}
motion = vec2(2,0)
invaderNumber = 1
end
It’s not set yet. I’ll set it in the test:
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[#army.invaders]
testInvader.pos.x = 240
army:update()
_:expect(army.overTheEdge).is(false)
end)
This is why globals are bad, class. Test runs. Now for the hard part:
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[#army.invaders]
testInvader.pos.x = 240
army:update()
_:expect(army.overTheEdge).is(false)
army:update()
_:expect(army.overTheEdge).is(true)
end)
This fails. Check some details:
_:expect(testInvader.pos.x).is(242)
Failed:
9: over right edge -- Actual: 240.0, Expected: 242
He wasn’t moved, apparently. What about after the next update?
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[#army.invaders]
testInvader.pos.x = 240
army:update()
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(242)
army:update()
_:expect(testInvader.pos.x).is(244)
_:expect(army.overTheEdge).is(true)
end)
Both checks for pos.x
return actual of 240. Our test invader isn’t moving at all. Obviously I don’t understand this code. Of course it has been literally two days since I wrote it, maybe more. Anyway, what’s up?
Oh bah! Army;update
does just one invader at a time. If we want to test this, we’ll have to move our lady directly. No problem:
_:test("over right edge", function()
local motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[#army.invaders]
testInvader.pos.x = 240
testInvader:update(motion, army)
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(242)
testInvader:update(motion, army)
_:expect(testInvader.pos.x).is(244)
_:expect(army.overTheEdge).is(true)
end)
And the tests runs fine. This gives me enough confidence, probably mistakenly, to see if they actually reverse on screen.
Well, they do. They seem to go further than I expected. I think I’ll draw a rectangle representing the game display: it may be wider than I think. More interestingly, they are obviously going faster to the left than to the right. At least it seems so to me.
How could that even be? Also they are not going down. I think I know what the dummy did. Here:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
if self.overTheEdge then
motion = motion*-1 - vec2(2,0)
self.overTheEdge = false
else
motion.y = 0
end
self.invaderNumber = 1
end
local inv = self.invaders[self.invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
Obviously that should be vec2(0,2)
That’s nearly as wrong as you can get in less than a dozen characters. Let’s write a test for this as a lesson to ourselves:
Arrgh, that’s going to be harder to do than I’d like, I need a much better setup. Let’s fix the code and then come back to the test.[^bet]
[^bet] Wanna bet he skips out on the test. Can’t trust him …
The good news is that it works, the invader persons march across, then ripple down and after one step down, march back across. The bad news is that the top rank is leading, not the bottom rank, because that’s how they are initialized: the first invader is at column zero, row zero.
We need to reverse their creation:
function Army:init()
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, Invader(p))
end
end
self.invaderNumber = 1
self.overTheEdge = false
end
A very dirty trick would be to insert the elements at position 1 instead of at the end. That is horribly inefficient, of course. But I’m going to do it to make sure it looks right:
function Army:init()
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, 1, Invader(p)) -- < ---
end
end
self.invaderNumber = 1
self.overTheEdge = false
end
That does work just fine, and you know what? I’m going to leave it that way. It only happens at init time, and it’s only 55 inserts. This is what computers are for.
OK. I keep thinking the screen is 244 wide, but it is 224. Better fix my test and code. Perhaps some globalish information is in order here, but that, too, is for later.
I think the marching is actually complete. But I’d really prefer a better test. It seems awfully silly to test something that works, though, doesn’t it? But what’s so hard about the test?
We want to test that when there has been an invader at the edge, and we have moved all the invaders, the motion global (arrgh) is set to the negative of what it was, and has -2 in its y. We should also test that once another update cycle has been completed, the motion variable’s y coordinate has been cleared. This is hard for two reasons. Here’s the Army update code where all this happens:
function Army:nextInvader()
if self.invaderNumber > #self.invaders then
if self.overTheEdge then
motion = motion*-1 - vec2(0,2)
self.overTheEdge = false
else
motion.y = 0
end
self.invaderNumber = 1
end
local inv = self.invaders[self.invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
So this code needs to run for all the invaders there are, and only then will it do its magic. Maybe we can make our test run on just one invader, like this:
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[1]
army.invaders = [testInvader]
...
But we still have to call the update in Army
, not just nextInvader
, so the test should become:
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[1]
army.invaders = [testInvader]
testInvader.pos.x = 220
army:update()
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(222)
army:update()
_:expect(testInvader.pos.x).is(224)
_:expect(army.overTheEdge).is(true)
end)
However, Army:update
has that clock stuff in it. It’s stubbed out but it won’t be forever:
function Army:update()
if self:itIsTimeToUpdate() then
self:resetTimeToUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(motion, self)
end
end
end
We could extract the inner bits into a method doUpdate
…
function Army:update()
if self:itIsTimeToUpdate() then
self:resetTimeToUpdate()
self:doUpdate()
end
end
function Army:doUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(motion, self)
end
end
Then our test can call doUpdate
and it should all be good:
_:test("over right edge", function()
motion = vec2(2,0)
local army = Army()
local testInvader = army.invaders[1]
army.invaders = {testInvader}
testInvader.pos.x = 220
army:doUpdate()
_:expect(army.overTheEdge).is(false)
_:expect(testInvader.pos.x).is(222)
army:doUpdate()
_:expect(testInvader.pos.x).is(224)
_:expect(army.overTheEdge).is(true)
end)
(Yes, there was a typo in the previous version. The compiler told me.)
OK, that runs. What have I left undone? Ah, that global, motion. Let’s init that as a member variable in Army. Easy enough, just use the rather limited Codea “find” to find them all and type self.
. If you need more detail, check the code, but you don’t.
Let’s sum up.
Summing Up
Have I mentioned that summing up has value even if we’re not writing an article? After we complete some bit of stuff …
Hey, wait, I forgot to commit: marching band and forth and down.
Summing up has value, in part because in so doing, we might remember something we forgot. It’s also a good way to remember what we’ve learned, and to think a bit about what’s next.
We still haven’t dealt with clocking down to every 60th of a second. I think that might be worth doing right now, lest we forget that too.
In the spike, we had this:
if elapsed < self.lastElapsedTime + self.timeToUpdate then return end
That was conditioned by this:
self.lastElapsedTime = ElapsedTime
self.timeToUpdate = 1/60
I’m a bit uncomfortable using 1/60 exactly. That could easily knock us down to 1/30 if we were just a bit slow off the blocks. But otherwise it looks good. But we can do better, I think:
function Army:init()
self.invaders = {}
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, 1, Invader(p))
end
end
self.invaderNumber = 1
self.overTheEdge = false
self.motion = vec2(2,0)
self.updateDelay = 1/65 -- less than 1/60th by a bit
self:resetTimeToUpdate()
end
function Army:update()
if self:itIsTimeToUpdate() then
self:resetTimeToUpdate()
self:doUpdate()
end
end
function Army:doUpdate()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
end
function Army:itIsTimeToUpdate()
return ElapsedTime > self.nextUpdateTime
end
function Army:resetTimeToUpdate()
self.nextUpdateTime = ElapsedTime + self.updateDelay
end
That seems to work. It’s hard thinking whether we should set 1/65 or 1/55 isn’t it? I think I did it right …
Commit: update no faster than 1/60th.
Where we we? Oh, right. Summing up. Good idea, it reminds you of stuff you meant to do, or need to do later.
I keep forgetting the screen dimensions. 224, 224. Remember that. Oh, and I wanted to show the screen bounds. Let’s do that.
Ha. Another mistake. I poked this into Main’s draw function:
function draw()
--if true then return end
pushMatrix()
pushStyle()
background(40, 40, 50)
showTests()
scale(4) -- makes the screen 1366/4 x 1024/4
translate((1366-1024)/8,0)
rectMode(CORNER) -- <---
fill(0) -- <---
rect(0,0,224,256) -- <---
stroke(255)
fill(255)
TheArmy:draw()
drawGunner()
drawMissile()
popStyle()
popMatrix()
TheArmy:update()
end
And I see that the invaders walk half-way off the edge on the right:
Why? Because they are drawn on center, so we don’t trigger the reversal until the center is over the line. We need to fix the test and the code. In prepping for that, I also notice that they start out about 1.5 times the width of an alien in from the left hand side (but do go half over the line on the way back). That’s because they are initialized to
for row = 1,5 do
for col = 1,11 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, 1, Invader(p))
end
end
So they start out at x = 16, not x = 0. And if they did, they’d overlap because they are drawn in CENTER
mode. Let’s fix.
function Army:init()
self.invaders = {}
for row = 0,4 do
for col = 0,10 do
local p = vec2(col*16, 256-row*16)
table.insert(self.invaders, 1, Invader(p))
end
end
self.invaderNumber = 1
self.overTheEdge = false
self.motion = vec2(2,0)
self.updateDelay = 1/65 -- less than 1/60th by a bit
self:resetTimeToUpdate()
end
function Army:draw()
pushMatrix()
pushStyle()
rectMode(CORNER)
for i,invader in ipairs(self.invaders) do
invader:draw()
end
popStyle()
popMatrix()
end
Now they start on the left (but still with black space at the top, I’m not clear what’s up with that). They step clear off at the right, of course, because now we need to change the right margin we use to trigger. I think that’ll need renaming.
Grrr. This is getting worse and worse. My rectangle isn’t centered, so my screen math is simply wrong. Clearly I need lunch and a break. So I’ll revert to “update no faster than 1/60” and take this up in the next session, after a little Pencil™ and Paper™ work.
Now then, summing up and not messing around.
Lesson 1 was “be sure to sum up, looking back to see what happened and what needs to happen”.
Lesson 2 is probably “don’t rush to hammer things in at the last minute”.
Any rational author would edit all those mistakes out. I am a different kind of rational author: I’m here to show you that we do make mistakes, sometimes really dumb ones. It’s part of the biz.
Next time we’ll do some screen math, and settle on our object and screen model to keep things on the screen, no more and no less.
See you then!