Space Invaders 38 - Casual Saturday
Some recovery from repo problems, some work with the clock, whatever else seems right.
Because I somehow got Working Copy confused yesterday, I’m still making sure I’ve reproduced what I had accomplished. In addition, I’ve seen some issues with timing, with invaders seeming to slow down and so on. So today I’m just going to mess about with things. I’ll go carefully, but I might skip some steps here in the article. Some of you may thank me for that.
I should mention right here that I do very much like the Working Copy app (@WorkingCopyApp on Twitter), and have had excellent support from Anders, its creator. We’re not sure what happened yesterday, but it’s starting to look like something went wrong in the communication between WorkingCopy and the file system. Once the glitch happened, everything I did just made it more weird.
It was easy enough to scarf recent copies of the code from Working Copy’s repo, from Codea where applicable, and even from the article’s pasted code. Nonetheless, I’m not certain quite where we’re at. The zip file at the end of this will be current with the code, and assuming all continues swimmingly, my repo will be up to date as well.
The timing issues have been strange. I know that Codea will call draw
on a cycle of 1/120th of a second if the iPad responds that quickly, will scale down to 1/60th if need be, and even to 1/30th if the iPad responds slowly enough. My iPad will usually manage to operate at 1/120th, but it occasionally drops down to 1/60th, then after a while often goes back to the higher speed. I suspect this means that something is going on in the background of iPadOS, since the game’s behavior is pretty consistent or even less busy as one shoots aliens.
The original Space Invaders game updated its graphics every 1/60th of a second, and in that interval would update only one invader, and some of the missiles, player, and bombs. I believe I read that some of those items were only updated every other time around. I may have mentioned that in a previous article. Maybe you remember: I do not. I write ‘em, I don’t read ‘em.
I want to match that behavior to some reasonable degree. First, I think it’s fun to have arbitrary but somehow reasonable constraints to work under. It makes building the thing more interesting to me. Second, to make the game’s overall speed the same, I need to match the original’s cycle.
I’m trying a new way of doing that, using Codea’s DeltaTime capability. The code now looks like this:
function GameRunner:update()
self:updateAlways()
self:update60ths()
end
function GameRunner:updateAlways()
SoundPlayer:update()
end
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
TheArmy:update()
end
My thinking was: there are some things that want to update every 1/60th of a second and no more often. (I’m not sure what to do if the game ever drops to 1/30. On my machines, I think that will never happen.) There are other things that may want to update as rapidly as possible (although I really don’t know of any.)
So I decided that the SoundPlayer will update on whatever the current cycle is, and keep track of its own timing. That should make it easier than deferring its decisions to someone else. All that manages so far is the descending tones of the march of the invaders.
And then the Army wants to update every sixtieth of a second. The problem is this:
Codea provides DeltaTime
and ElapsedTime
, which are the time since the previous draw
to the current one, and the total game time, respectively. DeltaTime and ElapsedTime do not vary during a draw cycle: they’re set once at the beginning of each cycle.
The problem is, these are both floating point values scaled in seconds. So 1/60th would be 0.016666 and 1/120th would be 0.008333 and 1/30th would be 0.033333 and so on. But there’s no guarantee that I can see that you’ll get exactly one of those values. In fact, in a simple test, DeltaTime varies between 0.0083381666627247 and 0.0083381666772766, flipping back and forth between those values.
So I’ve not seen a really super way of getting the cycle we’re currently on. Thus the current check:
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
TheArmy:update()
I decided arbitrarily that if DeltaTime is less than 0.016, it must be that we’re at 120x and otherwise not. So I have a little variable time120
that ticks by 1s if I think we’re at 120x and 2’s otherwise, and then if it’s at 2 or more, runs the updates that follow.
Kind of yucchy, but I don’t have a simple idea that I like better. Feel free to send me one.
The good news is that this seems to work. I’ve patched the sound player to 55/60ths of a second to change tones:
self.toneInterval = 55/60
self.toneTime = 333
self.toneNumber = 3
function Sound:update()
self.toneTime = self.toneTime + DeltaTime
if self.toneTime >= self.toneInterval then
self.toneTime = 0
self.toneNumber = (self.toneNumber + 1) %4
self:playRaw(self.tones[self.toneNumber+1])
end
end
And that makes the sound and the motion of the invaders synchronize:
The actual speed of the tone change is table-driven, as I mentioned yesterday, selected by how many aliens are currently alive. We don’t happen to have that number yet, but we do have the tables.
Note, though, that I’ve got the sound update working on actual time, not a counter. The original game trusted its counter to be a constant. I don’t have a counter that I trust and so I’m using the sum of DeltaTime.
Kind of grubby, but that’s what I’ve got right now, and it seems to work.
Hm, for something to do, let’s see about adjusting the tone interval. Should we count the live aliens when we want to know the new value, should we keep track of them as they die, or should we count them as part of the update process?
I’m kind of thinking it should be easy to count them during the update. Let’s look at that code.
function Army:update()
updateGunner()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
for b,bomb in pairs(self.bombs) do
b:update(self)
end
end
function Army:nextInvader()
self:handleCycleEnd()
local inv = self.invaders[self.invaderNumber]
self.invaderNumber = self.invaderNumber + 1
return inv
end
function Army:handleCycleEnd()
if self.invaderNumber > #self.invaders then
self.invaderNumber = 1
self:adjustMotion()
end
end
Curiously, we don’t know how many we processed. We process them all, and some of them, who are not alive, tell us to continue processing:
function Invader:update(motion, army)
if not self.alive then return true end
self.picture = (self.picture+1)%2
self:possiblyDropBomb(army)
self.pos = self.pos + motion
Shield:checkForShieldDamage(self)
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
end
Now we could call back to Army if we process an alien, and Army could reset the count at cycle end. I think I like that better than counting them, and better than having them report death. I’m not sure I can explain quite why I prefer this idea. It’s something about being a more direct measure than keeping track based on events: if he’s alive he reports in.
Let’s try that:
function Invader:update(motion, army)
if not self.alive then return true end
army:reportingForDuty()
self.picture = (self.picture+1)%2
self:possiblyDropBomb(army)
self.pos = self.pos + motion
Shield:checkForShieldDamage(self)
if self:overEdge() then
army:invaderOverEdge(self)
end
return false
end
Then we’ll do reportingForDuty
:
function Army:reportingForDuty()
self.invaderCount = self.invaderCount + 1
end
Now we need to zero that, and we need to record the final total.
function Army:init()
...
self.armySize = 55
self.invaderCount = 0
self.invaderNumber = 1
self.overTheEdge = false
self.motion = vec2(2,0)
self.bombs = {}
end
function Army:handleCycleEnd()
if self.invaderNumber > #self.invaders then
self.armySize = self.invaderCount
self.invaderCount = 0
self.invaderNumber = 1
self:adjustMotion()
end
end
I think that’s likely right. But I’d like to know for sure, so I’ll display the value in displayScore
:
text("INVADERS: " .. tostring(TheArmy.armySize), 144, 20)
Interestingly, but not surprisingly, this value only changes at the end of an update cycle, which is almost one second long when there are 55 aliens. But it seems to be right, so let’s see about using it in the sound player.
(I suppose I should have TDD’d that. Certainly could have. But I didn’t. I’m going to forgive myself.)
The sound update looks like this:
function Sound:update()
self.toneTime = self.toneTime + DeltaTime
if self.toneTime >= self.toneInterval then
self.toneTime = 0
self.toneNumber = (self.toneNumber + 1) %4
self:playRaw(self.tones[self.toneNumber+1])
end
end
I guess we should only update the interval at the end of a cycle, which will be after we’ve played tone number 3. So … just jammin here …
function Sound:update()
self.toneTime = self.toneTime + DeltaTime
if self.toneTime >= self.toneInterval then
self.toneTime = 0
self.toneNumber = (self.toneNumber + 1) %4
self:playRaw(self.tones[self.toneNumber+1])
self:updateToneTime()
end
end
function Sound:updateToneTime()
if self.toneNumber ~= 3 then return end
self.toneTime = self.toneCounts[self:invaderIndex()]/60
end
We are to look up the tone count in the toneCounts table, based on the index in the invaderCounts table that is … hmm, what. The table goes
{0x32, 0x2B, 0x24, 0x1C, 0x16, 0x11, 0x0D, 0x0A, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}
I guess we want the index such that the number of invaders is greater than or equal to the table value? So …
function Sound:invaderIndex()
local n = TheArmy.armySize
for i,c in ipairs(self.invaderCounts) do
if n >= c then return i end
end
return 1
end
I really should have test driven this. In fact, for my sins, I’m going to write some tests for it. After I run it to see what happens. And what happens is weird. The first tone switch goes fast so instead of dum-dum-dum-dum it’s dadum-dum-dum. And it never gets faster.
Let’s do some testing. First, we’ll make that invaderIndex function take an argument. I’ll write the test that forces that:
_:test("55 invaders is 0x34 cycles", function()
local i = SoundPlayer:invaderIndex(55)
_:expect(i).is(0x34)
end)
That makes me pass in the invader count here:
function Sound:invaderIndex(armySize)
for i,c in ipairs(self.invaderCounts) do
if armySize >= c then return i end
end
return 1
end
I’ll change the real call now as well, as this is essentially a refactoring:
function Sound:updateToneTime()
if self.toneNumber ~= 3 then return end
self.toneTime = self.toneCounts[self:invaderIndex(TheArmy.armySize)]/60
end
Now let’s see what our test does.
Well the test is wrong if we’re calling the index function, it should be checking for 1, the index, not the new tone time value. We’ll get to that.
_:test("55 invaders is 0x34 cycles", function()
local i = SoundPlayer:invaderIndex(55)
_:expect(i).is(1)
end)
This better run. And it does. Now a few more values:
_:test("55 invaders is 0x34 cycles", function()
local i = SoundPlayer:invaderIndex(55)
_:expect(i).is(1)
i = SoundPlayer:invaderIndex(1)
_:expect(i).is(16)
i = SoundPlayer:invaderIndex(10)
_:expect(i).is(8)
end)
The index lookup is right. That makes me very certain that the tone value looked up will also be right, since we have:
function Sound:updateToneTime()
if self.toneNumber ~= 3 then return end
self.toneTime = self.toneCounts[self:invaderIndex(TheArmy.armySize)]/60
end
Reading that code shows me the bug. I want to set toneInterval
, not toneTime
.
function Sound:update()
self.toneTime = self.toneTime + DeltaTime
if self.toneTime >= self.toneInterval then
self.toneTime = 0
self.toneNumber = (self.toneNumber + 1) %4
self:playRaw(self.tones[self.toneNumber+1])
self:updateToneInterval()
end
end
function Sound:updateToneInterval()
if self.toneNumber ~= 3 then return end
self.toneInteval = self.toneCounts[self:invaderIndex(TheArmy.armySize)]/60
end
It’s still not getting faster. Weird. Oh. it should help a bit if I write interval
instead of inteval
.
That works just as intended. Commit: invader tones speed up.
In playing the game to listen to the sounds, I’m almost certain I saw a bomb fall from an empty column. Ah. I see what it is. Bombs start from the invader position, randomized a bit for height, to give them a better chance of penetrating:
function Invader:dropBomb(army)
army:dropBomb(Bomb(self.pos - vec2(0,16 + math.random(0,3))))
end
But positions are lower right corner. Invaders are 16 wide. We should add 8 to x, er but we’re subtracting. Wow this isn’t clear:
function Invader:dropBomb(army)
army:dropBomb(Bomb(self.pos - vec2(-8,16 + math.random(0,3))))
end
But it’s correct. I’m going to leave it, because I’m tried of playing.
Commit: bombs drop from middle of alien.
Enough. Just messing around. No big lessons other than the usual that testing makes things easier, and that it doesn’t find all one’s typos.
See you next week for a real session.