It could happen: I’ve figured out a good way to handle these callbacks.

Last night I was trying to think of ways to handle timing things that might be better, triggered by my description yesterday of how one might wish one could do it:

    vanish()
    waitAwhile()
    while(notSafeToComeBack())
        waitAwhileMore()
    end
    comeBack()

I looked into coroutines, which are very cool but way too fancy, and couldn’t do this job anyway. I looked to see if there was some kind of non-busy time delay in Lua or Codea. And I looked again at the description of tween.delay. It looks like this:

Syntax
tween.delay( duration )
tween.delay( duration, callback )
tween.delay( duration, callback, … )

This function creates a tween that simply waits for the specified duration. You might use this to create a pause within a tween.sequence, or to delay the calling of a particular function.

Any additional arguments specified after the callback parameter will be passed as arguments to the callback function.

Reading that last paragraph finally poked an idea into the right memory cell in my brain. It answers the question “how can you make a tween call a method on an object”, although it doesn’t seem to be talking about that at all.

In Codea/Lua, a class is a table of functions. An instance has a copy of that table, or a magic link to it: I really don’t know or care which, though I’d bet it’s a copy.

When you say to your object, an instance of Ship:

    myShip:drawAt(position, rotation)

the draw function will probably refer to position and rotation, and also to self, the instance in hand, in this case myShip.

Codea does this by passing a secret first argument to functions called with the colon notation. That notation is exactly the same as saying:

    myShip.drawAt(myShip, position, rotation)

Fetch the drawAt function from the instance, and call it, passing the instance as the first parameter, as if its parameter name were self.

This is all so behind the curtains that it’s almost a miracle that I know it. It’s something we almost never think about. And it’s the core of a better solution to the callback messiness in Ship and other spots in Asteroids.

Let me show you what I mean:

Hyperspace

The current code for hyperspace looks like this:

function Ship:enterHyperspace()
    local tryToAppear = function()
        self:tryToAppear()
    end
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,tryToAppear)
end

function Ship:tryToAppear()
    local tryAgain = function()
        self:tryToAppear()
    end
    if self:safeToAppear() then
        U:addObject(self)
        self:dropIn()
    else
        self:signalUnsafe()
        tween.delay(3,tryAgain)
    end
end

Our tweens call local functions, to get bound to self and possibly other values. But there’s another way. I’ll type it in and we’ll look at it.

function Ship:enterHyperspace()
    U:deleteObject(self)
    self.pos = self:randomPointIn(100,200, WIDTH-100, HEIGHT-100)
    tween.delay(3,self.tryToAppear,self)
end

function Ship:tryToAppear()
    if self:safeToAppear() then
        U:addObject(self)
        self:dropIn()
    else
        self:signalUnsafe()
        tween.delay(3,self.tryToAppear,self)
    end
end

This is much neater, without those nested local functions, and, to my eyes at least, much more clearly correct. It may come to your mind to wonder whether the call in tryToAppear is perhaps recursive and might be a problem, but no, the tween call immediately returns and we’re out of the method, which is called again later, by the magic of tweens.

This makes me happy. Aside from needing to understand how anObject:doSomething() relates to doSomething(anObject), it’s base-level simple. I like that.

I’ll commit this: “remove nested functions from hyperspace`.

Other Tweens

Let’s look for other places where this might be better.

There’s this one, which I’m going to leave alone, as it’s a bit complex:

function Ship:die()
    local f = function()
        if U.attractMode then return end
        Score:instance():spawnShip()
    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

Then there’s this:

function Missile:init(pos, step)
    function die()
        self:die()
    end
    self.pos = pos
    self.step = step 
    U:addObject(self)
    tween.delay(3, die)
end

This is a perfect candidate:

function Missile:init(pos, step)
    self.pos = pos
    self.step = step 
    U:addObject(self)
    tween.delay(3, self.die, self)
end

Works perfectly. Commit: “remove nested function from missile tween die”.

Here’s one in Saucer:

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.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()
    self:startSound()
    tween.delay(7, die)
end

That check on being instance is there advisedly. If the saucer has been shot down or hit an asteroid, it has already died. The tween doesn’t know that. We can still remove this nesting with a new method:

function Saucer:init(optionalPos)
    Instance = self
    U:addObject(self)
    self.size = Score:instance():shouldDrawSmallSaucer() and 0.5 or 1
    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()
    self:startSound()
    tween.delay(7, self.dieQuietlyIfInstance,self)
end

function Saucer:dieQuietlyIfInstance()
    if self == Instance then
        self:dieQuietly()
    end
end

We can always mechanically reduce one of these nested functions by creating a non-nested one with a unique name, where we need to. This is a bit better and I’m going to keep it.

Commit: “remove nested function from saucer tween”.

function Score:stopGame()
    local f = function()
        self.gameIsOver = false
        U.attractMode = true
    end
    self.gameIsOver = true
    if not U.attractMode then tween.delay(5,f) end
end

This is interesting. We have to be careful to reset gameIsOver, because that’s the flag that makes us display “GAME OVER” for a while. We’ll do this:

function Score:stopGame()
    self.gameIsOver = true
    if not U.attractMode then 
        tween.delay(5,self.enterAttractMode,self) 
    end
end

function Score:enterAttractMode()
    self.gameIsOver = false
    U.attractMode = true
end

Commit: “remove nested function in score tween”.

There’s this one:

function Splat:init(pos)
    local die = function()
        U:deleteIndestructible(self)
    end
    self.pos = pos
    U:addIndestructible(self)
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, die)
end

Splat doesn’t have a die function, other than this nested one. We’ll export that one to get this:

function Splat:init(pos)
    self.pos = pos
    U:addIndestructible(self)
    self.size = 2
    self.diameter = 6
    self.rot = math.random(0,359)
    tween(4, self, {size=10, diameter=1}, tween.easing.linear, self.die, self)
end

function Splat:die()
    U:deleteIndestructible(self)
end

Commit: “remove nested function in splat tween”.

Now, individually, most of these were simple enough, and followed a pattern, so arguably they were not worth changing. However, we now know that they aren’t the best pattern, and if we leave them in the code, future programmers not as familiar as we are with the situation might copy the wrong ones, especially if there are several. It’s better to get to a consistent pattern.

We’re left with this one:

function Ship:die()
    local f = function()
        if U.attractMode then return end
        Score:instance():spawnShip()
    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

We now have a pattern to use to extract this, so we can do it almost by rote. (I’d try never to do anything totally by rote, because thinking is important.)

function Ship:die()
    U:playStereo(U.sounds.bangLarge, self, 0.8)
    Explosion(self)
    U:deleteObject(self)
    Instance = nil
    if not U.attractMode then
        tween.delay(6, self.spawnUnlessAttractMode, self)
    end
end

function Ship:spawnUnlessAttractMode()
    if U.attractMode then return end
    Score:instance():spawnShip()
end

Program works as advertised. Commit: “remove nested function in ship die tween”.

Summing Up

Not bad at all. In well under two hours, we’ve removed all the nested functions from all the tweens in the whole program, replacing them with a better pattern. This is a perfect little example of reducing “technical debt”, the difference between the program we now know how to write and the one we’ve actually written over time.

A good morning’s work, and it’s only 0830. And a good advertisement for reading and thinking about the documentation for the system you’re using.

Since it’s so early, you never know, I might come back and do some more. Or I might take the morning off.

See you next time!

Asteroids.zip