Let’s clean up that level handling a bit.

The new level handling works well, but it could benefit from being a bit less awkward and a bit more clear. I’ve got an idea. Here’s what we have now:

function Universe:defineLevels()
    self.t10 = {}
    self.t20 = {}
    self.t30 = {}
    self.t40 = {}
    self.t50 = {}
    self.t60 = {}
    self.t70 = {}
    self.t80 = {}
    self.t90 = {}
    self.drawLevels = {}
    self.drawLevels.backgound = self.t10
    self.drawLevels.ship = self.t20
    self.drawLevels.saucer = self.t30
    self.drawLevels.asteroid = self.t40
    self.drawLevels.missile = self.t50
    self.drawLevels.splat = self.t60
    self.drawLevels.fragment = self.t70
    self.drawLevels.score = self.t80
    self.drawLevels.buttons = self.t90
end

function Universe:drawEverything()
    local tables = {self.t10, self.t20, self.t30, self.t40, self.t50, self.t60, self.t70, self.t80, self.t90}
    for i, tab in ipairs(tables) do
        for k,o in pairs(tab) do
            self:drawProperly(o)
        end
    end
end

When people register, it’s like this:

function Ship:init(pos)
    self.pos = pos or vec2(WIDTH, HEIGHT)/2
    self.radians = 0
    self.step = vec2(0,0)
    self.scale = 2
    self.drawLevel = U.drawLevels.ship
end

function Ship:makeRegisteredInstance(pos)
    if Instance then U:deleteObject(Instance) end
    local ship = Ship(pos)
    Instance = ship
    U:addObject(ship)
    return ship
end

Everyone takes a draw level from U, and then U uses it to fill in the appropriate table, in a two-step process for adding, because we don’t want to add when looping in the collision finder:

function Universe:addObject(object)
    self.addedObjects[object] = object
end

function Universe:applyAdditions()
    for k,v in pairs(self.addedObjects) do
        self.objects[k] = v
        v.drawLevel[k] = v
    end
    self.addedObjects = {}
end

Here we grab the drawLevel from the object being added, and tuck it away in objects which is used for collisions, and in the appropriate drawLevel table, which the object holds onto as a token. It could be a string or anything, really.

Arguably, it’s risky passing those tables around to everyone. If they messed with it, bad things could happen. But our objects don’t come marching in here busting things up, they collaborate and they don’t mess around with things they shouldn’t mess with. The table is just a pointer anyway, so no harm done. Still, worth thinking about. Thinking is good.

Anyway, let’s clean this up. I don’t really like the definition of the drawLevels:

function Universe:defineLevels()
    self.t10 = {}
    self.t20 = {}
    self.t30 = {}
    self.t40 = {}
    self.t50 = {}
    self.t60 = {}
    self.t70 = {}
    self.t80 = {}
    self.t90 = {}
    self.drawLevels = {}
    self.drawLevels.backgound = self.t10
    self.drawLevels.ship = self.t20
    self.drawLevels.saucer = self.t30
    self.drawLevels.asteroid = self.t40
    self.drawLevels.missile = self.t50
    self.drawLevels.splat = self.t60
    self.drawLevels.fragment = self.t70
    self.drawLevels.score = self.t80
    self.drawLevels.buttons = self.t90
end

Why does it name those tables with weird names and then assign them to the keys people look up. Why can’t we just assign the tables directly, like this:

function Universe:defineLevels()
    self.drawLevels = {}
    self.drawLevels.backgound = {}
    self.drawLevels.ship = {}
    self.drawLevels.saucer = {}
    self.drawLevels.asteroid = {}
    self.drawLevels.missile = {}
    self.drawLevels.splat = {}
    self.drawLevels.fragment = {}
    self.drawLevels.score = {}
    self.drawLevels.buttons = {}
end

That’s like twice as simple and since I’ve already made one mistake in that method yesterday, simple is better.

We can’t do that, however, because of this:

function Universe:drawEverything()
    local tables = {self.t10, self.t20, self.t30, self.t40, self.t50, self.t60, self.t70, self.t80, self.t90}
    for i, tab in ipairs(tables) do
        for k,o in pairs(tab) do
            self:drawProperly(o)
        end
    end
end

Because the names t10, t20 and so on defined the order. Let’s take it as written that we’ll use the names instead:

function Universe:drawEverything()
    local drawingOrder = {"background", "ship", "saucer", "asteroid", "missile", "splat", "fragment", "score", "buttons" }
    for i, name in ipairs(drawingOrder) do
        for k,o in pairs(self.drawLevels[name]) do
            self:drawProperly(o)
        end
    end
end

This works, because given a table named drawLevels, these two Lua expressions are exactly the same thing:

drawLevels.xyz
drawLevels["xyz"]

The former expression in Lua literally means the second.

So what is shown above works and is more clear through being simpler, and because it more explicitly expresses drawing order. Now we do know that some of those levels aren’t used, background and buttons, but my preference is to leave them there because they’re harmless and if we move the design to a better place, we’ll likely normalize the background and the buttons into real objects anyway.

I wouldn’t argue for putting those things in before they are needed. They’re in because they expressed our thinking at the beginning of designing all this, and our thinking now. If we remove those statements, we’re losing a bit of the expression of our design in the code. YMMV, but I’d leave them in now that they’re there.

Let’s commit: Simplified drawing order and drawLevels.

Now there’s one more thing. Remember that we needed to clear the asteroids table after exiting attract mode, before creating a new wave, and we had to do it longhand:

function Universe:newWave()
    local pos
    self.beatDelay = 1 -- second
    self.timeOfNextWave = 0
    local t = self.drawLevels.asteroid -- < ---
    for k, o in pairs(t) do
        t[o] = nil
    end -- < ---
    for i = 1, self:newWaveSize() do
        if math.random(0,1) then
            pos = vec2(0,math.random(HEIGHT))
        else
            pos = vec2(math.random(WIDTH), 0)
        end
        Asteroid(pos)
    end
end

We had to do that because of that weird t10 stuff, because those tables were permanent, and unlike most objects, the asteroids in attract mode never get destroyed.

We don’t have to do that any more, we can do this:

function Universe:newWave()
    local pos
    self.beatDelay = 1 -- second
    self.timeOfNextWave = 0
    self.drawLevels.asteroid = {}
    for i = 1, self:newWaveSize() do
        if math.random(0,1) then
            pos = vec2(0,math.random(HEIGHT))
        else
            pos = vec2(math.random(WIDTH), 0)
        end
        Asteroid(pos)
    end
end

That’s a bit nicer. Commit: simplify clearing asteroids before new wave.

That’s all for now, save for the traditional summing of the up.

Summing Up

One pattern I’ve noticed in these articles is that I tend to get things working, and usually pretty close to right, in one session, and then in a day or two I come back with a better idea. It seems to help me to step away. I think there are two things going on.

First, when my head is deep into the problem and current solution, there’s no room for more details. So getting my head out of the muck helps.

Second, I do often think about how the program works, and how it might work, and about how the language works, and about how everything in the world works … and sometimes that gives me ideas.

So stepping back … and thinking. Possibly something to try.

At a higher level, I’m just delighted that this little game has given me so many opportunities to think about our craft, profession, art, call it what you will. Throughout these articles, from the very first one back on May 11th, we’ve had a working version of the program.

Oh, admittedly the first version just had a round asteroid and no ship, but that code both served as the foundation for everything that has come since then, and it looked enough like an asteroid that you could show it to your gramma and say Look, Gramma, what I made!

And every day after that, things got better looking and better crafted.

This is still a very small program. But every program is made of small bits, and every program can be kept as clear as this one if we keep at it. It’s far from perfect but it’s even further from the worst I’ve ever seen – or, probably, the worst I’ve ever done.

Soon, we’ll need a new program to work on. Soon.

See you next time!

Asteroids.zip