Space Invaders 67
I’ve convinced myself that a global would be a better way to handle constants than the singleton. Kill your darlings.
The current syntax:
Constants:instance():yourNameHere()
is just too much. It seems to me to be obscuring what’s going on. Let’s try something like this:
Constant:yourNameHere()
Should be an easy conversion but we’ll preserve both forms at least for a while.
One question is where to create the global. I think it should be at the top of GameRunner’s init:
function GameRunner:init(numberOfLives)
Constant = Constants()
self.lm = LifeManager(numberOfLives or 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
Now we should be able to change references at will:
function Army:initMemberVariables()
self.weaponsAreFree = false
self.armySize = 55
self.invaderCount = 0
self.invaderNumber = 1
self.overTheEdge = false
self.motion = Constants:instance():invaderSideStepVector()
self.stepDown = Constants:instance():invaderDownStepVector()
self.bombDropCycleLimit = 0x30
self.bombCycle = 0
self.saucer = Saucer(self)
self.score = 0
end
becomes:
function Army:initMemberVariables()
self.weaponsAreFree = false
self.armySize = 55
self.invaderCount = 0
self.invaderNumber = 1
self.overTheEdge = false
self.motion = Constant:invaderSideStepVector()
self.stepDown = Constant:invaderDownStepVector()
self.bombDropCycleLimit = 0x30
self.bombCycle = 0
self.saucer = Saucer(self)
self.score = 0
end
Test. Works. Let’s do the rest of the changes in Army, then test and commit:
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
function Army:processMissile(aMissile)
if aMissile.v > 0 and aMissile.pos.y <= Constant:missileTop() then
self:checkForKill(aMissile)
end
end
Looks good. Commit: convert Army to new constant format.
function Saucer:go(shotsFired)
self.startPos, self.stopPos, self.step = Constant:saucerStartInfo()
if shotsFired%2==1 then
self.step = -self.step
self.startPos,self.stopPos = self.stopPos,self.startPos
end
self.alive = true
self.pos = self.startPos
self.exploding = 0
Runner:soundPlayer():play("ufoLo")
end
Works. Commit: converted Saucer to new constant format.
function MarshallingCenter:marshalArmy(army)
local bottomY = Constant:invadersBottomY()
local startY = bottomY + 5*16
local invaders = {}
for row = 1,5 do
for col = 11,1,-1 do
local p = vec2(col*16, startY-row*16)
table.insert(invaders, 1, Invader(p,self.vaders[row], self.scores[row], army))
end
end
return invaders
end
Commit: converted MarshallingCenter to new constant format.
function Missile:handleOffScreen()
if self.pos.y > Constant:missileTop() then
self.explodeCount = 15
end
end
function Missile:explosionColor()
return self.pos.y > Constant:missileTop() and color(255,0,0) or color(255)
end
Works: commit: converted Missile to new constant format.
OK, I think I like that better. Now, for a bit of safety, let’s rename the class Constants to GameConstants. The class name is a bit too close to the global name.
Done. Commit: rename Constants class to GameConstants.
Display Improvements
We have some display anomalies. The Touch to Start is right on top of the top row of invaders, and the test results are too low:
Let’s move the Touch to Start up by 24 pixels, a rank and a half, and see if that leaves room for the tests.
function drawTouchToStart()
pushStyle()
stroke(255)
fill(255)
fontSize(50)
text("Touch to Start", WIDTH/2, HEIGHT-300)
end
This isn’t good. This text is being drawn in regular screen scale. We should draw it in game scale. That’s going to be a bit tricky. The same will be true for the tests, I imagine.
This will take a bit of organization. Or … we do have the ability to compute the scale factors. Let’s use them “in reverse” to position the Touch to Start and tests.
We can get scale from this method:
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
We’ll also want to get at the saucer level and go a bit above it. With a little refactoring, we extract that information:
function GameConstants:saucerStartInfo()
local saucerY = self:saucerY()
return vec2(8,saucerY), vec2(208,saucerY), vec2(2,0)
end
function GameConstants:saucerY()
return self:invadersBottomY() + 0x50
end
The saucerY
used to be inlined. I broke it out so that I could do this:
function drawTouchToStart()
pushStyle()
stroke(255)
fill(255)
fontSize(50)
local sc = Runner:scaleAndTranslationValues(WIDTH,HEIGHT)
local y = (Constant:saucerY() + 0x10)*sc
text("Touch to Start", WIDTH/2, y)
end
Here I compute the y coordinate as if scaled as 16 above the saucer. Then I scale it and use it.
I decided to do the same thing with the tests:
function showTests()
if not CodeaUnit then return end
if not CodeaVisible then return end
pushMatrix()
pushStyle()
fontSize(50)
textAlign(CENTER)
...
local sc = Runner:scaleAndTranslationValues(WIDTH,HEIGHT)
local y = (Constant:saucerY() + 0x20)*sc
text(Console, WIDTH/2, y)
popStyle()
popMatrix()
end
And the result is quite nice:
So that’s fine.
Would you believe …
I did the GameConstants refactoring late last night after having heard enough of the “debate”. Would you believe that I have an idea that I think might be a bit better? Well, I have. It goes like this:
How Codea Lua Classes Work
A Codea Lua class is just a table. As a table, it contains hashed key-value pairs, where each value is either a simple value like a string or number, or a function. If it’s a function, it acts as a method, and if it’s a value, it’s a member. Lua includes a bit of syntactic sugar to translate
myObject:someOp(a,b,c)
into
myObject.someOp(myObject, a, b, c)
A function defined as
function MyClass:someOp(a,b,c)
Is sugared into
MyClass.someOp = function(self,a,b,c)
Then there’s one more trick, which is that when we write
MyClass = class()
The class
function sets up the initial table in the global variable MyClass
with enough stuff to create new instances and call their init
. Voila! Classes and instances.
What if there can be only one?
If we only want one of these things, which is the case for our constants, what if instead of this:
GameConstants = class()
...
Constant = GameConstants()
What if we just said this:
Constant = {}
And then defined all our methods directly on Constant
?
Constant would behave exactly like an instance of GameConstants. But since it’s not defined as a class, you can’t make another instance. There can be only one.
I’m going to try that in a moment, but first I want to talk about all this switching around.
It’s just one damn thing after another
One of the things I learned rather late in life was that despite my excellent design sense and my amazing ability to imagine how an interface should be, I was far from perfect at it. You know that thing that happens when you adopt someone else’s library, and nothing seems quite right? Well, when I’d create my own designs without using them, and finally got around to using them … nothing seemed quite right. Ideas that seem good in the head often turn out to be not quite as good in the code.
So I learned to try things as a user of the things, not just the designer of the things. In essence that’s what we’re doing when we use TDD1 on a new object: we become its first user, and as the user, our experience feeds back into the design, making it better.
In this little program I’ve tried many things and then changed them. Most recently there have been various attempts to reduce the number of global variables, use of singletons, and so on. Yesterday I decided that for constants, I preferred a global to the singleton syntax.
Today, I’m going to build everything into that global, which will result in saving … approximately one line. Why? Not to save the line, but because I want to try the technique, and I think it will be just a bit nicer.
Here goes!
Constants, Phase III
At least III. Maybe IV or even MCMLXVII. Presently our constants class looks like this:
GameConstants = class()
local singleton
function GameConstants:instance()
if not singleton then singleton = Constants() end
return singleton
end
function GameConstants:init()
end
function GameConstants:invadersBottomY()
return 0x78
end
function GameConstants:saucerRunningLimit()
return self:invadersBottomY() - 2*self:invaderDownStep()
end
... and more ...
In GameRunner, we create our instance, Constant:
function GameRunner:init(numberOfLives)
Constant = GameConstants()
self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
Shield:createShields()
...
I’ll remove that line right now. Poof, gone. Now in GameConstants, I’ll rename it to Constant, make it a table, not a class, remove the init and singleton stuff, and replace the name GameConstants with Constant throughout:
Constant = {}
function Constant:invadersBottomY()
return 0x78
end
function Constant:saucerRunningLimit()
return self:invadersBottomY() - 2*self:invaderDownStep()
end
function Constant:invaderDownStep()
return 8
end
... and more ...
If I’m not sorely mistaken, everything should run just as before. Oddly enough, I’m not sorely mistaken. I’ll rename the GameConstants tab to Constant, and I think I’ll add a warning comment because this is a new technique in our code base.
-- Constant
-- RJ 20201008
-- Note: not a class, a single object
Constant = {}
Commit: Constant converted to single object.
We’ll live with this a while. Possibly there will be some reason not to like it, but I think it’ll be OK.
You might be wondering about member variables rather than functions. We can have those in the usual fashion. A method on Constant can define and refer to self.myVar
just as we would in an instance, because whenever we call a method, Constant itself is passed in. If we wanted to define a bunch of them, we’d give it an initialize
or init
method. Right now, even our literal constants are returned from methods, so we don’t use this ability, but we could.
What else? I think that’s enough for one article, and I have a video call at 9, so let’s wrap up.
Wrapping Up
So, a few things in this article, which encompasses two sessions, a quick one last night and another this morning.
We revised the handling of constants not once but twice, to what I think is a good result. And we improved the look of the startup display a bit. I think the new constants object helped with that, because it gave me rather quick access to the information I needed to adjust the display.
I did use the scaling information from GameRunner in doing that. That’s OK, because Main knows the Runner, but it’s a bit odd that the GameRunner would have scaling built in, given that its main function is running the game. Possibly that would be better if broken out somewhere. I don’t see where offhand, and it’s just the one thing. It used to be used only once. Now it’s used three times. That does argue for giving it some attention.
But we probably won’t. We have other invaders to fry.
I think I like the new Constant object. It has just the single global, its name, and there is only one by its very nature. It’s a true singleton in that sense, and unless we for some reason need to change the constants of the universe, I think this design approach will stand.
What will we do tomorrow? I honestly don’t know.
Oh, and a request. If you are reading these articles, and have questions or comments, do feel free to tweet at me or even email questions or ask on the Codea forum. I will benefit from the feedback and will answer your questions if I can. If you want to remain anonymous, let me know and I’ll refer to you as “A Reader” or something.
See you soon!
-
Test-Driven Development, a technique where we write a failing test, make it pass, improve the code, repeat. (I’ve been asked to define my terms more often, so I thought I’d try it.) ↩