Asteroids 56
Some tweaks to hyperspace seem in order, but I feel that this thing is nearly done.
Like it says up there. I want to either remove the sound it makes when we decide not to re-enter real space, or change it. A little chirp might be OK. There is code checking for realSpace
that need not check any more, because I moved to removing the ship from the objects collection while it’s in hyperspace.
That’s rather nice. There are a number of checks in the original 6502 code for whether the ship is in hyperspace. Mostly, we don’t need to check, because things “just won’t happen” when it’s not in the objects collection.
I’d like to expand the safety radius for re-entry, as it is still too easy to get hit right after emergence. I’ve also created a little visual “effect” on return that I like, and we need to decide whether to retain that or adhere to the original game’s approach of bringing you in, in a fairly safe spot, and letting the player spot the ship as best they can.
I looked through the 6502 code pretty carefully to see whether there really is a hyperspace failure that automatically destroys you, and I’m not saying there isn’t, but I am saying that I didn’t find it. I did find some code that seems to randomly refuse to take you into hyperspace, but it is rather obscure and I’m not sure about it. It’s certainly the case that if you need to go to hyperspace and the system doesn’t let you go, you’re likely to die, because you generally only do it when you’re about to die.
Anyway we’ll do that cleanup. Then we’ll check our list, which we don’t have, to see what else needs to be done to “match” the original game.
One item on my mind is the screescreescree noise that the saucers make. That is done with a looped sound, which works well with one exception: it’s not in stereo. As the ship goes across the screen, I feel like its scree should pan from side to side. We’ll have to change how it’s generated to make that happen.
Anyway let’s begin by trying to harvest a quick win with the realSpace
flag.
The realSpace Flag
Codea isn’t marvelous about editing power but it has a decent search that you can use to find all occurrences of a string:
We have five occurrences of the flag, including one that sets it true and one that sets it false. The other three are:
function Ship:draw()
if self.realSpace then
self:drawAt(self.pos, self.radians)
end
end
function Ship:move()
if self.realSpace then
if U.button.hyperspace then
self:enterHyperspace()
else
if U.button.turn then self:turn() end
if U.button.fire and not self.holdFire then self:fireMissile() end
if not U.button.fire then self.holdFire = false end
self:actualShipMove()
end
end
end
function Ship:enterHyperspace()
local appear = function()
if self:safeToAppear() then
U:addObject(self)
self.realSpace = true -- <---
self.scale = 10
tween(1, self, {scale=2})
else
self:signalUnsafe()
tween.delay(3, self.hyperReturn)
end
end
self.realSpace = false -- <---
U.objects[self] = nil
local w = math.random(WIDTH-200)
local h = math.random(HEIGHT-300)
self.pos = vec2(w,h) + vec2(100,200)
self.hyperReturn = appear
tween.delay(3,appear)
end
Since the ship is not in objects
any more, I believe that the draw and move will not be called, and there’s no reason to have the flag any more. Let’s remove it and see what happens.
(This is nowhere nearly as good as “let’s remove it and see what tests break”. Our tests are weak. We know this, so we are careful, but we are human and things can go wrong. Just now, though, I feel we’ll be OK.)
I commented out all the cases, a bad habit left over from life before Working Copy and Git, then tested. The program works fine. I’ll complete the edit and commit “removed realSpace flag”.
So that’s nice.
I watched the arrival effect, and I rather like it, so I’ve decided to leave it in:
I’d like to find a better sound to indicate that we’re not returning from hyperspace because of danger, but for now, I think I’ll just remove the sound altogether. I was using the “free ship” sound, which isn’t appropriate:
We have a function broken out:
function Ship:signalUnsafe()
sound(U.sounds.extraShip)
end
We’ll just remove the sound and leave the function. Why? Because it expresses some intention here, even though we do not currently signal at all:
function Ship:signalUnsafe()
-- no signal implemented at this time
end
We could of course remove this function and edit the code that calls it, but I think this expresses our actual design better than doing that.
Commit: “remove unsafe return sound”, and take a break.
Scree
Our next mission is to change the saucer’s “scree” sound to be in stereo, panning across as it travels. Why? Because we want to.
How? Well, presently, as we’ll find in the code in a moment, the saucer just starts a looped sound when it arrives, and stops the sound when it leaves. The actual sound is quite short, a fraction of a second. We’ll change things so that the sound calls our playSound
function in the universe, which accepts stereo information, ad we’ll call it repeatedly, updating the location each time.
Let’s see how that works:
function Saucer:init(optionalPos)
function die()
if self == Instance then
self:dieQuietly()
end
end
Instance = self
U:addObject(self)
if Score:instance():shouldDrawSmallSaucer() then
self.size = 0.5
self.sound = sound(asset.saucerSmallHi, 0.8, 1, 0, true)
else
self.size = 1
self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)
end
self.shotSpeed = 3
self.firstShot = true
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:setRandomTurnTime()
tween.delay(7, die)
end
We see there that we select which sound to play, and the true
at the end of the sound call says to loop it. How does it stop?
function Saucer:dieQuietly()
self.sound:stop()
U:deleteObject(self)
Instance = nil
U.saucerTime = U.currentTime
end
Let’s refactor this to make a place to stand. We do this often, whenever what we’re about to do needs a little room to figure itself out.
Notice in the init
that sauce size and saucer sound are both handled in the same if structure. That’s OK, but it’s not great. Those ideas both relate to “large or small saucer” but one sets up saucer drawing and the other sets up saucer sound. It’s arguably just a nicety, but I’d prefer those to be broken apart. I wanted to create two methods, initiateSound
and stopSound
, but the “sound” name isn’t really appropriate if we include size.
I’ll go in two steps, to see what happens:
function Saucer:init(optionalPos)
function die()
if self == Instance then
self:dieQuietly()
end
end
Instance = self
U:addObject(self)
self.size = Score:instance():shouldDrawSmallSaucer() and 0.5 or 1
if Score:instance():shouldDrawSmallSaucer() then
self.sound = sound(asset.saucerSmallHi, 0.8, 1, 0, true)
else
self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)
end
self.shotSpeed = 3
self.firstShot = true
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:setRandomTurnTime()
tween.delay(7, die)
end
Here, I’ve pulled out the size setting. That construct is part of our local coding standard. You might prefer an if structure. But if you had one, you should do to it what I’m about to do to the one that’s in there:
function Saucer:init(optionalPos)
function die()
if self == Instance then
self:dieQuietly()
end
end
Instance = self
U:addObject(self)
self.size = Score:instance():shouldDrawSmallSaucer() and 0.5 or 1
self:startSound()
self.shotSpeed = 3
self.firstShot = true
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:setRandomTurnTime()
tween.delay(7, die)
end
function Saucer:startSound()
if Score:instance():shouldDrawSmallSaucer() then
self.sound = sound(asset.saucerSmallHi, 0.8, 1, 0, true)
else
self.sound = sound(asset.saucerBigHi, 0.8, 1, 0, true)
end
end
This is desirable anyway, since we like to have our functions either do things or make decisions, but not both. (Yes, I know that assignment has a decision in it but the line counts as “do something”. At least well enough to satisfy me.
Now to make the corresponding stop:
function Saucer:dieQuietly()
self:stopSound()
U:deleteObject(self)
Instance = nil
U.saucerTime = U.currentTime
end
function Saucer:stopSound()
self.sound:stop()
end
This “should work”. We’ll test and commit “refactor saucer sound”.
Now we have a place to stand. We need to set up a tween delay to play the sound in stereo. We need to field the termination of that delay with a callback to play the sound (again). And we need to stop the tween when the saucer dies. I propose the following:
function Saucer:startSound()
local play = function()
U:playStereo(self.sound, self)
self.sounder = tween.delay(self.soundDelay, self.playFunction)
end
self.playFunction = play
self.soundDelay = 0.2
if Score:instance():shouldDrawSmallSaucer() then
self.sound = asset.saucerSmallHi
else
self.sound = asset.saucerBigHi
end
U:playStereo(self.sound, self)
self.sounder = tween.delay(self.soundDelay, self.playFunction)
end
function Saucer:stopSound()
tween.stop(self.sounder)
end
This looks intricate. This may help:
function Saucer:startSound()
local play = function()
U:playStereo(self.sound, self)
self.sounder = tween.delay(self.soundDelay, self.playFunction)
end
self.playFunction = play
self.soundDelay = 0.2
self.sound = self:selectSound()
U:playStereo(self.sound, self)
self.sounder = tween.delay(self.soundDelay, self.playFunction)
end
function Saucer:selectSound()
if Score:instance():shouldDrawSmallSaucer() then
return asset.saucerSmallHi
else
return asset.saucerBigHi
end
end
We have a fair amount that had to be done here:
- set up a callback function to repeat the sound
- select the sound to play
- remember the sound for repeats
- set up the desired delay
- play the sound (once to start and then in repeats)
- remember the tween id so we can stop it.
Still, it’s all in these three little functions and now the saucer sound is in stereo. Life is good.
Commit: saucer sound is stereo. There’s something I’d like to discuss at this point.
Modularity Works
I’m famous in a small circle for saying “modularity works”, by which I mean, well, if you build code that is cohesive and loosely coupled, things go better. We see a good example of that right here.
In the 6502 code, handling of hyperspace, saucer sounds, and all that jazz is sprinkled all over the code with little skips and jumps. The author had to remember to put in all those skips and jumps in the right places, or the game wouldn’t work.
Now in those days, we didn’t have what we have now, fast chips and big memory. Yes, even my iPad is much faster, with much more memory, than the original Asteroids. So I can afford to break things out into classes and functions, where in those days, we couldn’t do that quite so freely.
Now there are still plenty of functions in the original Asteroids, and some of them are even very cleverly arranged to be able to deal with more than one kind of thing, perhaps a ship or a saucer or a missile. But those things had to be created with great cleverness and care.
Now I’m not arguing that I’m less clever now than I was then – and I did implement Spacewar from scratch back then, although I don’t think I ever built Asteroids. But at 80 years old, possibly I am less clever. Be that as it may, I know and use tools that were not ready to hand in those days.
Those tools, my tests, classes. functions, and high-level facilities like tweens, are deployed to assist me in building my programs. They are not deployed automatically: they are deployed by me, using judgment, brains, and maturity, in order to arrange my life so that features can go in easily from the first day to the last, so that things that should be done only once tend to be done only once, so that things that are alike tend to be together, and so on.
That’s why today’s efforts went smoothly, and why almost all the things we’ve done so far on this program have gone smoothly: our approach helps us.
A really good example was breaking out the start and stop sound notions. It might have been possible to edit what we needed into the middle of what we started with. It would not have been easy. By breaking the issue of sound away from the rest of the concerns, I was able to focus more clearly both on what it had to do, and on how to do it.
More and better tests would have made some of what we’ve done here easier. Whether they’d have helped with today’s efforts, I’m not at all sure: I don’t see how to have used tests effectively.
But tests, as important as they are, are only one of the tools in our kit. The big section for the tools we used today is called “modularity”.
Modularity works.
We’re two hours in, with a nifty new stereo sound and a smoother patch of code for the saucer. Let’s call it a day.
Getting close to done-done here, I think. Let’s see what comes up. See you next time!