Asteroids 45
Run tests automatically, dribs, drabs, and oddities. Pretty calm.
I made a tweak to CodeaUnit to save feature name and run summary to a string and return it from execute
. Ran into an oddity with Working Copy: I had done the CodeaUnit change on my old iPad, and sent the zipped CodeaUnit to this one. Renamed the old CodeaUnit to C2deaUnit, and unzipped. Working Copy was still looking at the old file, despite the rename. Nothing I thought of could sort it, so I pasted the new code into the old file, deleted, renamed, and now all’s good. I may have to start using remotes for CodeaUnit.
I’ll include a copy of the current CodeaUnit in this article.
With that summary info being saved accessibly, I modified Asteroids to run the tests automatically and display the results on the startup screen:
local Console = ""
function setup()
runTests()
U = Universe()
U:newWave()
end
function runTests()
if not CodeaUnit then return end
local det = CodeaUnit.detailed
CodeaUnit.detailed = false
Console = _.execute()
CodeaUnit.detailed = det
end
function draw()
U:draw(ElapsedTime)
if U.attractMode then
pushStyle()
fontSize(50)
fill(255,255,255, 128)
text("TOUCH SCREEN TO START", WIDTH/2, HEIGHT/4)
text(Console, WIDTH/2, HEIGHT - 200)
--print("//",Console)
popStyle()
end
end
And the result looks like this:
I may have to make it display in red and make a rude noise if the tests fail, to be sure that I notice. Not that I don’t trust myself.
Making this change drove out an interesting few problems. One was that the saucer screaming noise was triggered multiple times and didn’t stop when the tests ran. That was because the saucers were just discarded, not passing die
, not turning off their sounds. So I modified all the saucer tests to send die to the saucers.
That was tedious, and I suspect that ideally I’d have made a new feature about the saucers, and done the saucer creation and teardown in the before and after functions. Just killing them will do for now.
I’ve encountered another interesting issue though. Recall that ships automatically respawn, due to this code:
function Ship:die()
local f = function()
if U.attractMode then return end
Ship()
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
tween(6, self, {}, tween.easing.linear, f) -- <---
end
So when the tests create and destroy those ships, to stop the sound, the tween runs and triggers creation of a new ship. It seems that if I wait long enough after the tests run, that check for attractMode
works, but otherwise the tween hits at game time and tosses several ships into the mix. It’s very odd.
Now the real “fix” is probably to let ship spawning move up to Universe, where decisions about game and wave cycles are better made. For now, I think I’ll condition the tween on attractMode
as well:
function Ship:die()
local f = function()
if U.attractMode then return end
Ship()
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
if not U.attractMode then
tween(6, self, {}, tween.easing.linear, f)
end
That’s probably redundant but I think it’ll fix the problem.
I was mistaken, and finally sorted out why: the FakeUniverse didn’t have attractMode
set, so saucers created in the fake universe fired their tweens and so if you started the game right after the tests ran, you got the multiple ship effect. Setting the mode in the fake universe seems to have sorted that.
There’s another thing that I noticed, which is that it is possible for the ship to destroy two objects that crash into it at the same time. I don’t think this should be allowed to happen. The relevant code is this:
function Universe:findCollisions()
for k, o in pairs(self.objects) do
for kk, oo in pairs(self.objects) do
o:collide(oo)
end
end
end
Suppose we are processing an object ‘o’ in the outer loop, and suppose it is within collision range of two or more objects. Even though the first one kills it, it’ll still be being compared with all the other objects. So, rarely, it might kill again.
I’m going to protect against that. Should I write a test for it? Yes, but I’m not going to. At least not right now at 5 AM.
function Universe:findCollisions()
for k, o in pairs(self.objects) do
for kk, oo in pairs(self.objects) do
if self.objects[o] then o:collide(oo) end
end
end
end
Since we set objects[o]
to nil on death, this check should ensure that we don’t process objects after they’ve passed from this mortal plane.
Commit: “autorun codeaunit, no duplicate ships, no duplicate hits”.
Civilized Hour: 0850
Well, I still don’t see a good way to test that loop code. The reason is that the order in which pairs
presents objects is literally undefined, so I don’t see a way to ensure there’s something in the outer loop seeing items in the inner loop, two of which can destroy him, before they see him first.
Anyway I’m sure enough of the code. Sometimes you gotta do what you gotta do.
What do we gotta do now?
I was thinking, maybe we should change over to Universe-centric control over respawning and the like. That got me wondering if I could have somehow stopped the tweens that were starting the ship over. That led to rereading the tween documentation with less focus on a specific thing to do, which led me to tween.delay
, which does a pure delay with callback, and tween.stop
, which, given the id returned from a tween
call (who knew?) will stop that tween.
The former will let us simplify some tweens:
function Ship:die()
local f = function()
if U.attractMode then return end
Ship()
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
if not U.attractMode then
tween(6, self, {}, tween.easing.linear, f)
end
end
We’ll try:
function Ship:die()
local f = function()
if U.attractMode then return end
Ship()
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
if not U.attractMode then
tween.delay(6, f)
end
end
That works nicely indeed. I think we’re safe from the extra ships now but we could imagine saving the tweens somewhere and stopping them. Thing is, for some purposes, like the explosion and splat, autonomous timing is great, but for ship spawning, it may not belong in Ship
at all. Let’s find any others we may be using.
The splat uses one to expand its size, so it’s good as it is. Missile just uses the delay, so we’ll fix that:
function Missile:init(pos, step)
function die()
self:die()
end
self.pos = pos
self.step = step
U:addObject(self)
tween.delay(3, die)
end
Much less odd and obscure. A small win with these. Saucer has one much like the splat:
function Saucer:init(optionalPos)
function die()
if self == Instance then
self:dieQuietly()
end
end
Instance = self
U:addObject(self)
self.shotSpeed = 5
self.pos = optionalPos or vec2(0, math.random(HEIGHT))
self.step = vec2(2,0)
self.fireTime = U.currentTime + 1 -- one second from now
if math.random(2) == 1 then self.step = -self.step end
self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)
tween.delay(7, die)
end
Those are nice. Commit: “use tween.delay”.
I think this is enough for the morning. Let’s quickly sum up.
Summing Up
Well, we’re displaying test results on the screen at startup, and we cleaned up a few little things.
Not much to see here. Next time I’ll try to think of something interesting. Maybe finite number of ships to game over.