Asteroids 60 - A good idea!
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!