More with Singletons. And distractions. And no singletons.

Like it says. I plan this morning to convert a thing or two to my new singleton style, moving toward fewer global values and a bit more consistency in style. I am hoping that this will be a mostly boring article, more for the record of these changes, without excitement.

We’ll see.

What Globals Do We Have?

In the Main tab, I see these:

  • Runner: the GameRunner instance.
  • SoundPlayer: the instance of Sound.
  • Line: the line across the bottom of the screen.

The latter is amusing. We actually do need the Line, as part of the game’s charm is that the bombs, if they make it to the bottom of the screen, damage the Line:

line damage

The Line is drawn in GameRunner:

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(Line,8,16)
    ...

It is damaged in Bomb:

function Bomb:damageLine(bomb)
    local tx = bomb.pos.x - 7
    for x = 1,6 do
        if math.random() < 0.5 then
            Line:set(tx+x,1, 0,0,0,0)
        end
    end            
end

Its creation and those two references are all the direct references to the line. I don’t think that rises to the level of requiring a Line object, but we should probably move it inside GameRunner.

I really had in mind working on singletons, but here we are. I think first, I’ll leave it global but bring it inside GameRunner:

function GameRunner:init()
    self.lm = LifeManager(3, self.spawn, self.gameOver)
    Shield:createShields()
    Player:newInstance()
    TheArmy = Army()
    Line = image(208,1)
    for x = 1,208 do
        Line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

This was just a code move. Nothing should change. And everything works fine. Se could commit and ship. I’ll commit just for practice: moved Line creation to GameRunner.

We could ship this version. Harvesting the value of small changes, GeePaw Hill says. This is that. Now let’s put the line into a member variable as well as the global:

function GameRunner:init()
    self.lm = LifeManager(3, self.spawn, self.gameOver)
    Shield:createShields()
    Player:newInstance()
    TheArmy = Army()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    Line = self.line
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

That affects no one. Test anyway. Still good. Now publish it:

function GameRunner:bottomLine()
    return self.line
end

Unused function. Harmless.

Now use the new line in draw. Should have done that before publishing but no harm done.

function GameRunner:drawStatus()
    pushStyle()
    tint(0,255,0)
    sprite(self.line,8,16)
    textMode(CORNER)
    fontSize(10)
    text("SCORE " .. tostring(TheArmy:getScore()), 144, 4)
    tint(0,255,0)
    local lives = self.lm:livesRemaining()
    text(tostring(lives), 24, 4)
    local addr = 40
    for i = 1,lives-1 do
        sprite(asset.play,40+16*i,4)
    end
    if lives == 0 then
        textMode(CENTER)
        text("GAME OVER", 112, 32)
    end
    drawSpeedIndicator()
    popStyle()
end

Still working. Now use the published one:

function Bomb:damageLine(bomb)
    local tx = bomb.pos.x - 7
    local line = Runner:bottomLine()
    for x = 1,6 do
        if math.random() < 0.5 then
            line:set(tx+x,1, 0,0,0,0)
        end
    end            
end

I cached it for clarity and, I suppose, performance. Still works.

Remove the global:

function GameRunner:init()
    self.lm = LifeManager(3, self.spawn, self.gameOver)
    Shield:createShields()
    Player:newInstance()
    TheArmy = Army()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end -- <-- next line gone 
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

All still works a treat. Search for references. There is one in test: before, that was protecting the global from test damage. I’ll just remove it.

All test green, game works. Commit: removed global Line.

Well, that was a distraction, in that we didn’t apply singleton, but it met the goal of reducing globals. Now what?

Now What?

We have SoundPlayer, and GameRunner left as globals known to Main. Let’s do Sound.

Sound is a bit tricky because of this:

function Sound:init(noisy)
    if noisy == nil then
        self.noisy = true
    else
        self.noisy = noisy
    end
    self.sounds = {}
    ...

It can be created to be noisy or not. We use the “not” capability in the tests, to avoid lots of noise while they run. Let’s resolve that with a new method: beSilent() when the time comes.

There are only two creators of Sound, the one in Main and one in tests, but there are many references to it. We could make it a singleton distinguished object. That was my plan a moment ago. But why can’t we just give it to the GameRunner and people can ask for it there? That’s less mechanism.

Yeah. Just because I love my new singleton technique doesn’t mean we should use it everywhere.

Chet Hendrickson and I have observed that when teams used to get the Gang of Four patterns book, they would start using various patterns, whether they needed them or not. They just wanted to work with whatever pattern caught their fancy. We used to call that behavior “Small boy with a pattern hammer”. I’ll try to avoid that.

I’ll follow a similar approach to Sound that I did with Line, but bigger steps:

function GameRunner:init()
    self.lm = LifeManager(3, self.spawn, self.gameOver)
    Shield:createShields()
    Player:newInstance()
    TheArmy = Army()
    self.soundPlayer = Sound() -- <---
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

function GameRunner:soundPlayer()
    return self.soundPlayer
end

Now replace all the references to SoundPlayer with Runner:soundPlayer(). I’ll spare you the printing.

Tests fail:

26: Player counts shots fired -- Player:147: attempt to call a table value (method 'soundPlayer')
function Player:unconditionallyFireMissile(silent)
    self.missile.pos = self.pos + vec2(7,5)
    self.missile.v = 1
    self.missileCount = self.missileCount + 1
    if not silent then Runner:soundPlayer():play("shoot") end
end

This is a test. What about the game? Oh, dammit. Can’t name the method and the variable the same.

function GameRunner:init()
    self.lm = LifeManager(3, self.spawn, self.gameOver)
    Shield:createShields()
    Player:newInstance()
    TheArmy = Army()
    self.sounder = Sound()
    self.line = image(208,1)
    for x = 1,208 do
        self.line:set(x,1,255, 255, 255)
    end
    self:resetTimeToUpdate()
    self.weaponsTime = 0
end

function GameRunner:updateAlways()
    self.sounder:update()
end

function GameRunner:soundPlayer()
    return self.sounder
end

Tests are green, game sounds play. And the tests make noise when they run.

In before we have:

        _:before(function()
            --Missile = {v=0, p=vec2(0,0)}
            --createBombTypes()
            SoundPlayer = Sound(false)
        end)

We need the beSilent() function now, on Sound:

function Sound:beSilent()
    self.noisy = false
end

Now we can simplify creation:

function Sound:init(noisy)
    self.noisy = true
    self.sounds = {}
    ...

Now we want to say this, but we may need to create a runner first.

        _:before(function()
            --Missile = {v=0, p=vec2(0,0)}
            --createBombTypes()
            Runner:soundPlayer():beSilent()
        end)

And yes, we do need a Runner. I wanted to make it local to the tests, but I can’t see how to do that. We’ll let it be global and Main will recreate it anyway–for now.

        _:before(function()
            Runner = GameRunner()
            Runner:soundPlayer():beSilent()
        end)

Curiously, I’m still getting one explosion sound when the tests run.

In addition … the game itself is silent … but only if I run the tests twice. I think I’ll build beNoisy and use it in after, but we’ll still have to find that one explosion.

function Sound:beNoisy()
    self.noisy = true
end

        _:before(function()
            Runner = GameRunner()
            Runner:soundPlayer():beSilent()
        end)

        _:after(function()
            Runner:soundPlayer():beNoisy()
            Player:clearInstance()
        end)

Sounds are back but I noticed that the game starts playing if I run the tests with the test button. I’ll not deal with that now. But the explosion sound has to go. I wonder if we’re creating a new Runner.

There are two:

        _:test("Bomb hits gunner", function()
            Runner = GameRunner()
            _:expect(Player:newInstance()).isnt(nil)


        _:test("Screen scaling", function()
            local g = GameRunner()
            local sc,tr
            sc,tr = g:scaleAndTranslationValues(1366,1024)
            _:expect(sc).is(4)
            _:expect(tr).is((1366-4*224)/(2*4))
            sc,tr = g:scaleAndTranslationValues(834,1112)
            _:expect(sc).is(3.72, 0.01)
            _:expect(tr).is(0)
        end)

The second is harmless. Remove the first. One more thing. In after, we should remove the Runner we tampered with, just in case.

        _:after(function()
            Runner:soundPlayer():beNoisy()
            Runner = nil
            Player:clearInstance()
        end)

That also fixes the game starting up when it shouldn’t. There’s this in Main:

function touched(touch)
    if Runner and Runner:livesRemaining() ~= 0 then
        Player:instance():touched(touch)
    else
        if touch.state == ENDED then
            testsVisible(false)
            startGame()
        end
    end
end

So if we leave a global Runner lying about, we’ll start the game … and then create a new Runner:

function startGame()
    Runner = GameRunner()
    invaderNumber = 1
end

So that’s fine. We’ve now removed the SoundPlayer global.

Tests run, game runs. Commit: remove SoundPlayer global.

Summing Up

I came in here expecting to apply my new Singleton Hammer to reduce the number of globals and it turned out that I didn’t need the hammer at all. I needed two new member variables in GameRunner, and a few tiny methods.

The good news is that doing what we did was less complex than setting up more singletons, and the better news is that I was–well, not “smart enough”–not stupid enough to do the more complex thing because I like it.

Tradeoffs

These changes were not without cost. We now reference things with more code. Where we used to say SoundPlayer, now we have to say Runner:ssoundPlayer(), and that’s surely a bit harder to understand (though not much) and a bit slower (though not much).

Globals are easy to reference, and in a small program they can be a decent solution. As the program gets larger, we get into more and more trouble managing the names ad the objects. When I write these little programs, I apply some “big program” ideas, creating classes and methods, using structures and approaches that we could do without if our programs are small enough.

Now, I’ve been doing this a long time, and I think that has changed me compared to others, or to past me:

In favor of these ideas, I am quite sure that they help me even in the small. Once there is a Sound object, I know immediately where to look to find all the sound stuff. The objects break out separate ideas into separate program entities, and that helps me. It might help you, you might sense the help.

But it might not help you, at least at first. It’s a different kind of thinking to find things in a more object-oriented program, and a different kind of thinking to create one. In early days working this way, it will surely feel awkward and confusing. I am sure it has been worth it for me. You get to decide about you,

In any case, it’s my pastime, and I hope you enjoy and get some useful ideas. All four of you.

See you next time!

Invaders.zip