Our mission today is to display the saucer score. Should be easy enough.

The saucer has a variable score that depends on how many shots the player has fired. We implemented that a while back:

function Saucer:init(army, optionalShotsFired)
    self.army = army
    self.exploding = 0
    self.scores = {100,50,50,100,150,100,100,50,300,100,100,100,50,150,100}
    if optionalShotsFired == nil then
        self.alive = false
    else
        self:go(optionalShotsFired)
    end
end

function Saucer:score()
    return self.scores[Player:instance():shotsFired()%15 + 1]
end

It’s not clear in my mind where we are supposed to display it. The code seems to say that it always displays to the right. I think for our purposes, we’ll put it off to the appropriate side. I’ll make it go on the right when the saucer is toward the left side of the screen, and on the left when the saucer is right. Otherwise it could go off screen.

In the original game, hitting the saucer, calculating its score, and displaying the score all happen more or less at once, as it cycles through displaying things. Our display is more asynchronous to execution, so we’ll need a slightly different approach.

Let’s see what saucer display looks like now:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        sprite(asset.saucer_exploding,self.pos:unpack())
        self.exploding = self.exploding - 1
    end
    popStyle()
end

I know from the original code that the score doesn’t display right away, I suppose so the player can enjoy the explosion for a bit. We can do something inside that self.exploding block. I’m starting to hate all these if statements, but we can decide later whether to get rid of them or not.

And I think it’s a good time to go for chai.

And with that done, let’s see what goes on here.

When the saucer is hit, we execute this:

function Saucer:killedBy(missile)
    if not self.alive then return false end
    if self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

function Saucer:die()
    self.alive = false
    self.army:addToScore(self:score())
    self.exploding =15
    Runner:soundPlayer():play("killed")
end

We should probably save the score at this point, because we want to display the actual value that was added, and the player might possibly fire again while we’re displaying. We can get the text from a number with string.format. Let’s begin with something really simple:

function Saucer:die()
    self.alive = false
    local sc = self:score()
    self.army:addToScore(sc)
    self.textScore = string.format("%d",sc)
    self.exploding =15
    Runner:soundPlayer():play("killed")
end

Then I’ll display it whenever we’re exploding:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        text(self.textScore,x+20,y)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

I unpacked the position so that I can use it twice, and for now I’m trying to display the text just to the right of the saucer, which is 16 wide. Now to test this, somehow. First I’ll just play until I get the saucer, I guess. No, I don’t like that answer already.

Instead, let’s just make anything I fire kills the saucer if it’s there.

function Saucer:killedBy(missile)
    if not self.alive then return false end
    return true
    if self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

Now I could wait until the aliens get low enough, or I could change when the saucer flies. In for a penny:

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    return true
    if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Player:instance():shotsFired())
    return true
end

Now, unless I miss my guess, the saucer will fly and when I fire, it’ll die, displaying its score.

Well, I do miss my guess because you can’t just return from the middle of a function, so we have to say this in both places:

function Saucer:killedBy(missile)
    if not self.alive then return false end
    if true then return true end
    if self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

Now then.

Well, the saucer doesn’t fly at all now. Better look more closely and maybe even do something a bit more sensible.

Oh. You should and understand read code before modifying it. Make a note of that.

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Player:instance():shotsFired())
    return true
end

This is the code that actually starts the saucer. Be a good idea if I let it execute.

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    if true or self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Player:instance():shotsFired())
    return true
end

Now then again:

What is wrong with me??? That line now unconditionally exits. One more try please, then I’ll go home.

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    --if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Player:instance():shotsFired())
    return true
end

OK, now the saucer runs. But it doesn’t seem to die. I imagine I’ve done the wrong thing again here:

function Saucer:killedBy(missile)
    if not self.alive then return false end
    if true then return true end
    if self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

Sure enough, I exit without actually killing the saucer. Must think about this but first let’s get my experiment to run.

function Saucer:killedBy(missile)
    if not self.alive then return false end
    if true or self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

I don’t quite see how to get a photo of this. No wait, another blow with my trusty hammer:

function Saucer:die()
    self.alive = false
    local sc = self:score()
    self.army:addToScore(sc)
    self.textScore = string.format("%d",sc)
    self.exploding =150
    Runner:soundPlayer():play("killed")
end

We’ll set the exploding delay to 150 which may give me time to snap a photo.

score

We see that the text is way too big, and that it’s starting on top of the explosion.

The first thing I check is that the saucer exploding image is 24 wide, not 16, even if the saucer is 16. So we’ll surely need to start about 30 pixels out, not the 20 I chose. Let’s also set the text mode and size:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        textMode(CORNER)
        fontSize(10)
        text(self.textScore,x+30,y)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

small score

That looks better. It needs to be red, and perhaps a bit lower down. I’m not sure quite why it would need that, but it seems that it does.

After some fiddling, this looks about right on the right side:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        textMode(CORNER)
        fontSize(10)
        fill(255,0,0)
        text(self.textScore,x+25,y)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

However, we want to display the text on the left, when the saucer is on the right half of the screen. How far to the left? Well, I have no idea. Conveniently, there is a textSize function. This allegedly returns the width and height of the text measured. I suspect it will not be sensitive to scale. I think I’ll just display that instead of the score to see what happens.

Does this seem like grotesque hackery to you? It does to me. But I’m in, and I’m staying in.

I guess I could have just printed this information at game startup but I displayed it instead, this way:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        textMode(CORNER)
        fontSize(10)
        fill(255,0,0)
        w = textSize(self.textScore)
        ws = string.format("%d",w)
        text(self.textScore,x+25,y)
        text(ws, x+25, y+10)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

I found that the smallest score (50) is 12 wide and the largest, 300, is 17. Let’s do some actual thinking here.

The center of the explosion, in the X direction, should be at x + 12, since the explosion is 24 wide. We want to start, on the right, at just a bit past the center, since we’re starting at 25 now. So that’ll be an additional 13. No, forget the center. Let’s just put it that when we’re on the left side of the screen, we’ll display the text at x + 25. On the right, we want to start, roughly, at x - w (the text width), but scaled. We might want to add or subtract a few, depending on how it actually looks.

For some reason I can’t explain, I think that scale() with no parameters may return the current scale. Let’s see. First, I’ll just make the score always appear to the left.

I’m mistaken about scale(). But wait, there’s the transformation matrix. No, too fancy. Let’s just save the darn scale when we compute it.

function GameRunner:scaleAndTranslate()
    local sc,tr = self:scaleAndTranslationValues(WIDTH, HEIGHT)
    scale(sc)
    translate(tr,0)
end

function GameRunner:scaleAndTranslationValues(W,H)
    local gameScaleX = 224
    local gameScaleY = 256
    rectMode(CORNER)
    spriteMode(CORNER)
    local sc = math.min(W/gameScaleX, H/gameScaleY)
    local tr = W/(2*sc)-gameScaleX/2
    return sc,tr
end

Caching those values makes some sense, though I could just ask the GameRunner again right in my display. Let’s do that instead.

I am just so wrong today. I really should go home, but I’m already home.

I don’t need to scale that value: the screen scale is whatever it is. So this should be a good first approximation to putting the score on the left:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        textMode(CORNER)
        fontSize(10)
        fill(255,0,0)
        w = textSize(self.textScore)
        text(self.textScore,x-w,y)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

It works nicely. I still want to lower the y a bit, though I’m not sure why that’s necessary. And then we’ll want to do the side to side thing. Then I’ll make a movie for us.

The explosion delay is too short to give a good view, let’s double it.

function Saucer:die()
    self.alive = false
    local sc = self:score()
    self.army:addToScore(sc)
    self.textScore = string.format("%d",sc)
    self.exploding = 30
    Runner:soundPlayer():play("killed")
end

I wind up tripling it. I recall that the original game waits a bit before displaying the score. We’ll try that as well. First, though, let’s do the side by side. Here’s our current draw code:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        local x,y = self.pos:unpack()
        sprite(asset.saucer_exploding,x,y)
        textMode(CORNER)
        fontSize(10)
        fill(255,0,0)
        w = textSize(self.textScore)
        text(self.textScore,x-w,y-2)
        self.exploding = self.exploding - 1
    end
    popStyle()
end

It’s well past time to extract a method for the explosion:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        self:drawExplosion()
    end
    popStyle()
end

function Saucer:drawExplosion()
    local x,y = self.pos:unpack()
    sprite(asset.saucer_exploding,x,y)
    textMode(CORNER)
    fontSize(10)
    fill(255,0,0)
    w = textSize(self.textScore)
    text(self.textScore,x-w,y-2)
    self.exploding = self.exploding - 1
end

Let’s extract drawExplosionScore as well:

function Saucer:draw()
    pushStyle()
    tint(255,0,0)
    if self.alive then
        sprite(asset.saucer, self.pos:unpack())
    elseif self.exploding > 0 then
        self:drawExplosion()
    end
    popStyle()
end

function Saucer:drawExplosion()
    local x,y = self.pos:unpack()
    sprite(asset.saucer_exploding,x,y)
    self:drawExplosionScore()
    self.exploding = self.exploding - 1
end

function Saucer:drawExplosionScore()
    textMode(CORNER)
    fontSize(10)
    fill(255,0,0)
    w = textSize(self.textScore)
    text(self.textScore,x-w,y-2)
end

Now we have a nice place to stand to put in the left-right stuff:

function Saucer:drawExplosionScore()
    textMode(CORNER)
    fontSize(10)
    fill(255,0,0)
    w = self:explosionScoreXOffset()
    text(self.textScore,x+w,y-2)
end

function Saucer:explosionScoreXOffset()
    return -textSize(self.textScore)
end

Note that I add in the offset in the test and return it negative from the function. This should leave things just as they were. Well, if x were still defined. Where did that go? Ah yes. Let’s pass it on down:

function Saucer:drawExplosion()
    local x,y = self.pos:unpack()
    sprite(asset.saucer_exploding,x,y)
    self:drawExplosionScore(x,y)
    self.exploding = self.exploding - 1
end

function Saucer:drawExplosionScore(x,y)
    textMode(CORNER)
    fontSize(10)
    fill(255,0,0)
    w = self:explosionScoreXOffset(x,y)
    text(self.textScore,x+w,y-2)
end

function Saucer:explosionScoreXOffset(x,y)
    return -textSize(self.textScore)
end

OK, now we’re good and we can return two different values from the offset function:

function Saucer:explosionScoreXOffset(x,y)
    if x > 112 then
        return -textSize(self.textScore)
    else
        return 25
    end
end

That works as intended, as you’ll see in the movie. But first let’s delay the display of the score a bit. I wound up extending the explosion time to 60 and displaying the score for a count of thirty. Too many magic numbers but it works as intended this way:

function Saucer:drawExplosion()
    local x,y = self.pos:unpack()
    sprite(asset.saucer_exploding,x,y)
    if self.exploding <= 30 then self:drawExplosionScore(x,y) end
    self.exploding = self.exploding - 1
end

saucer score

Now if I can remove all my hacks, I could commit this.

function Saucer:killedBy(missile)
    if not self.alive then return false end
    if self:isHit(missile) then
        self:die()
        return true
    else
        return false
    end
end

And

function Army:runSaucer()
    if self.saucer:isRunning() then return false end
    if self:yPosition() > Constant:saucerRunningLimit() then return false end
    if math.random(1,20) < 10 then return false end
    self.saucer:go(Player:instance():shotsFired())
    return true
end

I’ll check the diffs to make sure things are roughly as I expect. Yes, looks good. Commit: Saucer displays score when hit.

Game play seems correct as well. I think we’ll ship this. Time to sum up.

Summing Up

I made more than my quota of silly mistakes today, starting by at least two or three insertions of patches without remotely understanding what I was doing. Once I got rolling, things went better, and the basic result seems to me to be nearly good. We should do something about the magic numbers, which are now 60 and 30, and perhaps the magic right side width calculation (the 25) as well. But the structure of small methods seems decent to me.

As I assess myself now, my head and nose feel stuffy. Perhaps the fall allergies are hitting me harder than I expect. That could be affecting me.

I was aware early on that I was making silly mistakes. Had I been working with a pair, I surely would have mentioned it, and they would have noticed anyway, and they’d probably have picked up the slack. Very likely they’d have spotted my mistakes before I did in any case.

Without a pair, the wise thing to do might have been to pause, and possibly go home or turn my attention to something less challenging. Unfortunately, it takes more wisdom to do that than it does to recognize the problem. I really wanted to get something done today, including this article.

It has turned out well enough, and the actual implementation seems decent to me. The road was just a bit more bumpy than it needed to be.

I wonder whether some tests would have helped, but the logic here was trivial, and the main issue was getting it to look right on the screen, which is all about visible spacing. Perhaps I would have benefitted from a separate experiment displaying and adjusting values. A small tool, perhaps. It seems likely that it would have taken longer to do the tolling than to do the job, but I could be wrong. If we were going to do more of these things, a tool certainly might make sense.

What if there were a draw function like drawExplosionWithScore(...) with some useful parameters, that we could have called as part of drawStatus, that would show us what it would look like. Maybe it would even be controlled by some parameter sliders. That might be a decent compromise, easier to do than a tool, but also easier to check at run time.

Well, I guess hindsight is better than no sight at all. Maybe that idea will pop into my head sooner next time.

Anyway, for now, we have our new feature, and our new article, and we’ll see you next time!

Invaders.zip