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:

realspace

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:

arrival

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!

Asteroids.zip