Space Invaders 57
What shall we do today? I honestly don’t know.
I’m going to begin with a review of the code, paging through the tabs, to see what it makes me think about. If something really tasty pops up, I’ll probably divert from browsing and do it. If not, we’ll have a list of opportunities from which to pick.
I’m prepared to work on a code improvement, or on a feature, so both will turn up in my list.
Main
The Main tab is a bit of a mess. The first thing that comes to mind is this asymmetry between setup
and draw
:
function setup()
runTests()
parameter.boolean("Cheat", false)
Runner = GameRunner()
SoundPlayer = Sound()
createShields()
createBombTypes()
invaderNumber = 1
Lives = 4
Score = 0
Line = image(208,1)
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
end
function draw()
pushMatrix()
pushStyle()
background(40, 40, 50)
showTests()
Runner:draw()
popStyle()
popMatrix()
Runner:update()
end
We setup Runner, Shields, Bombs, Lives, Score, invader number and a line object. But then we only draw and update the Runner. Seems like a lot of that setup behavior should be somewhere else. I can only assume that Runner is drawing the shields:
function GameRunner:draw()
pushMatrix()
pushStyle()
noSmooth()
stroke(255)
fill(255)
self:scaleAndTranslate()
TheArmy:draw()
self.player:draw()
drawShields()
self:drawStatus()
popStyle()
popMatrix()
end
Yes and it calls a naked function to do it. Where is that function? Well, it’s down at the bottom of Main, together with others:
drawSpeedIndicator
rectanglesIntersectAt
rectanglesIntersect
createBombTypes
createShields
drawShields
addToScore
Some of these are general utility functions, like the rectangle ones. Some are calling for more structure where we have very little. We have no actual Score
class, just a global variable and a text display in GameRunner. But others do have better places to live. We have a Shield class, and a Bomb class. Surely those functions should be over there.
Well, as I suspected, these are a bit too obvious to leave alone. If you see a can near the trash bin, pick it up and toss it in. (Wear gloves, these days, but you get the idea.)
Let’s move createBombTypes into Bomb. Here’s its present code:
function createBombTypes()
BombTypes = {
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
BombExplosion = readImage(asset.alien_shot_exploding)
end
Better name for that my use the word asset rather than type. Let’s keep that in mind. How are these used?
Well! Here’s a discovery:
function Army:defineBombs()
local bombTypes = {
{readImage(asset.rolling1), readImage(asset.rolling2), readImage(asset.rolling3), readImage(asset.rolling4)},
{readImage(asset.plunger1), readImage(asset.plunger2), readImage(asset.plunger3), readImage(asset.plunger4)},
{readImage(asset.squig1), readImage(asset.squig2), readImage(asset.squig3), readImage(asset.squig4)}
}
local bombExplosion = readImage(asset.alien_shot_exploding)
self.rollingBomb = Bomb(vec2(0,0), bombTypes[1], bombExplosion, true, 1,16)
self.plungerBomb = Bomb(vec2(0,0), bombTypes[2], bombExplosion, false, 1,16)
self.squiggleBomb = Bomb(vec2(0,0), bombTypes[3], bombExplosion, false, 7,21)
end
The Army has subsumed this feature, or mostly so, doing the creation here. Can I just delete the global one? I’m going to do it and if something breaks, fix it.
Everything works fine, except that I don’t see the cheat switch that I can toggle to make the player bulletproof. I wonder where that went.
I did this in setup
:
function setup()
runTests()
parameter.boolean("Cheat", false)
print("post parm")
The phrase does not print. Furthermore, it appears that the tests have run twice. Can it be that my simple change to turn off tests didn’t work?
function runTests()
if not CodeaUnit then return end
local det = CodeaUnit.detailed
CodeaUnit.detailed = false
Console = _.execute()
CodeaUnit.detailed = det
end
Surely that didn’t do anything. But I’ll back it out.
Oh my. There are new tabs inserted, way off to the right, where they don’t show up. Working Copy has been hit by a change in iPadOS and it has saved me by putting its version of the program into separate tabs. If I delete those we should be OK.
Yes, that’s better. And the tests blow up calling createBombTypes, in
before`. I’ll delete that and work through whatever depends on that obsolete global.
My parameter is back, no surprise. But attempting to fire a missile, I get this:
Gunner:84: attempt to index a nil value (global 'Gunner')
stack traceback:
Gunner:84: in function 'fireMissile'
Gunner:13: in function 'touched'
That turns out to be a spurious tab “Gunner” that Working Copy inserted. Since I had renamed that tab to Player, it didn’t get a date and I didn’t notice it.
Now we get an error from this:
function Bomb:explosionBits()
return BombExplosion
end
Which is returning the global. Never noticed, because it kept working until now. Bombs know their explosion, so this should become:
function Bomb:explosionBits()
return self.explosion
end
At this point I decide to use Codea’s search for other opportunities but I think I’ve got them all.
Now I have a bit of a dilemma. I’m used to being able to commit my code, having finally built up a decent habit of doing so. But until the issue between Working Copy and iPadOS is resolved, I’m not trusting it to work. I guess I’ll do the commit and be careful about letting it write back. I rather wish it didn’t do that quite so automatically when it sees a sync error.
Commit: remove global BombTypes and BombExplosion. Recover Working Copy issues. Scary, though. Working Copy is really good but both that program and Textastic took a hit from the sudden release of iPadOS 14. Makes me tread more cautiously than I’d like.
OK, now let’s look at the Shield stuff. We have a Shield class for individual shields. We create them in Main tab, and draw them somewhere else, probably GameRunner.
These two global functions are in Main:
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 48
Shields = {}
for s = 1,4 do
local entry = Shield(img:copy(), vec2(posX,posY))
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
pushStyle()
for i,shield in ipairs(Shields) do
shield:draw()
end
popStyle()
end
Main setup
calls the create, and yes, GameRunner does the draw. And the Shields collection is global. Where is it accessed?
Well it’s accessed in the creation and drawing but also in this Shield method:
function Shield:checkForShieldDamage(anObject)
for i,shield in ipairs(Shields) do
local hit = shield:damage(anObject)
if hit then return true end
end
return false
end
This is called in Invader, Bomb, and Missile, as they can all damage the shields. This method, by the way, is treated as a class method, called against the class name Shield
. This is a bit naff, but I don’t have a better idea right now.
We do need a place to store all four shields. Historically in situations like this in Codea, I’ve put them in globals declared in the tab that owns them. This time I want to try to find something better. I wonder whether it’s possible to store data in the class, and have it show up in the instances. To find out, I think I’ll do a spike rather than mess up this app.
DataInClass
I create a new app, based on my CodeaUnit template, called DataInClass. I plan to TDD this spike just to keep my hand in.
My basic idea is to define a class, with some allegedly useful method or methods. Then define a function on the class that stores a table of information … and call it on the class, not on an instance. Then create a new instance and see if the data is there.
_:test("Data saved in class appears in instance", function()
SomeClass:saveData("saved")
local s = SomeClass()
_:expect(s.getData()).is("saved")
end)
Now to build the class:
SomeClass = class()
function SomeClass:init()
end
function SomeClass:getData()
return self.data
end
function SomeClass:saveData(aDatum)
self.data = aDatum
end
Let’s see what happens:
1: Data saved in class appears in instance -- SomeClass:7: attempt to index a nil value (local 'self')
Yes, that can’t work. We’ll change the save:
function SomeClass:saveData(aDatum)
SomeClass.data = aDatum
end
I meant to say that anyway but self
was just too natural. Now test again. Well, a typo in the test too, I said . and should have said :
_:test("Data saved in class appears in instance", function()
SomeClass:saveData("saved")
local s = SomeClass()
_:expect(s:getData()).is("saved")
end)
So this says to me that with some care, we can save what would have been global data in the class object, and it will be replicated into the instances. Let’s check that extra hard:
_:test("Data saved in class appears in instance", function()
SomeClass:saveData("saved")
local s = SomeClass()
_:expect(s:getData()).is("saved")
local t = SomeClass()
_:expect(t:getData()).is("saved")
_:expect(SomeClass:getData()).is("saved")
end)
So what we know is that whatever is saved in the class table will be replicated into the instances. And, as shows by that last expect
, it is of course accessible in the class as well.
Armed with this new understanding, let’s move the shields inside Shield.
Shields Inside Shield
The plan, insofar as I see it, is to move createShields
to be a Shield class method, and then move other references to the Shields` global inside the Shield class. In a fit of over-optimism, I decide to do the draw as well, in the same go. This cannot work out well.
The originals are:
function createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 48
Shields = {}
for s = 1,4 do
local entry = Shield(img:copy(), vec2(posX,posY))
table.insert(Shields,entry)
posX = posX + 22 + 23
end
end
function drawShields()
pushStyle()
for i,shield in ipairs(Shields) do
shield:draw()
end
popStyle()
end
As I look at that first method, it gives me fear. The table I’m trying to create will only exist after all the shields are created. So that means they won’t have it. But the class still will. I think we’re OK but aren’t going as far as the spike suggested we could. So I do this:
function Shield:createShields()
local img = readImage(asset.shield)
local posX = 34
local posY = 48
local t = {}
for s = 1,4 do
local entry = Shield(img:copy(), vec2(posX,posY))
table.insert(t,entry)
posX = posX + 22 + 23
end
Shield.shields = t
end
Now we have the references. One is here in the draw:
function Shield:drawShields()
pushStyle()
for i,shield in ipairs(Shield.shields) do
shield:draw()
end
popStyle()
end
The other was in this method:
function Shield:checkForShieldDamage(anObject)
for i,shield in ipairs(Shields) do
local hit = shield:damage(anObject)
if hit then return true end
end
return false
end
Which becomes:
function Shield:checkForShieldDamage(anObject)
for i,shield in ipairs(Shield.shields) do
local hit = shield:damage(anObject)
if hit then return true end
end
return false
end
Now we need to call the new create and draw, from Main and GameRunner:
function setup()
runTests()
parameter.boolean("Cheat", false)
Runner = GameRunner()
SoundPlayer = Sound()
Shield:createShields()
invaderNumber = 1
Lives = 4
Score = 0
Line = image(208,1)
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
end
function GameRunner:draw()
pushMatrix()
pushStyle()
noSmooth()
stroke(255)
fill(255)
self:scaleAndTranslate()
TheArmy:draw()
self.player:draw()
Shield:drawShields()
self:drawStatus()
popStyle()
popMatrix()
end
I expect some tests to explode but rather expect the game to work. I also expect to have my expectations dashed for what I hope will be a trivial error. I am delighted to report that three tests broke but the game plays fine.
Now to the tests:
31: Invaders step downward after hitting edge -- attempt to index a nil value
21: bomb drops below bottom-most live invader -- attempt to index a nil value
14: invader eats shield -- attempt to index a nil value
I reckon these will be relying on the old globals. Let’s see what they are:
_:test("invader eats shield", function()
local simg = readImage(asset.shield) -- 22x16
local i1img = readImage(asset.inv11) -- 16x8
local i2img = readImage(asset.inv12)
local shield = Shield(simg, vec2(100,100)) -- thru 121,115
Shields = {shield}
local iPos = vec2(110,110)
local invader = Invader(iPos, {i1img,i2img}, nil, nil)
local answer = Shield:checkForShieldDamage(invader)
_:expect(answer).is(true)
end)
Right, this sets up the global, which is no longer used. This may fix it:
_:test("invader eats shield", function()
local simg = readImage(asset.shield) -- 22x16
local i1img = readImage(asset.inv11) -- 16x8
local i2img = readImage(asset.inv12)
local shield = Shield(simg, vec2(100,100)) -- thru 121,115
Shield.shields = {shield}
local iPos = vec2(110,110)
local invader = Invader(iPos, {i1img,i2img}, nil, nil)
local answer = Shield:checkForShieldDamage(invader)
_:expect(answer).is(true)
end)
What is TRULY fascinating is that it fixed the other two as well, since now Shield.shields is initialized. But we’d better look at those and see whether the test cares about shield specifics.
And they do not reference shields, it’s just that the mechanism needed to be set up. So we’re good.
Let’s move the creation of the shields into GameRunner:
function GameRunner:init()
self.lm = LifeManager(3, self.spawn, self.gameOver)
Shield:createShields()
self.player = Player()
TheArmy = Army(self.player)
self:resetTimeToUpdate()
self.weaponsTime = 0
end
Tests are OK, game plays OK. Commit: Shields saved in Shield class, creation moved to GameRunner.
I am a bit concerned about this whole morning, because in response to my problems with Working Copy, I renamed a couple of Codea files. I am no longer quite certain what Working Copy is looking at. I guess I can check that with a trivial change to see if it picks it up.
It seems to be linked up. That’s comforting. I rarely use Working Copy’s ability to revert or recover but I like to know it’s there.
It’s Saturday, so let’s call it a morning. What have we learned?
Summing Up
We quickly found some good targets for quick improvements. Never had to move beyond the Main tab to find things worth doing.
The first change, BombTypes, showed me that while I had moved the capability into Bomb already, there was still a copy outside, and some code, at least the tests, was looking at that. This can happen intentionally: We could imagine that in a large program, we might do the improvement of putting the types inside, but do the refactoring to reference them incrementally, over time, as we processed different modules. We could even imagine deprecating the old way and giving various separate teams some amount of time to switch over.
In my case, however, it was nothing so glorious. I think I just forgot to delete the old one. If I did it on purpose that way, well, I’ve forgotten that.
The second change, the Shields, gave us a bit of learning, which is that if we store things in the class as opposed to an instance, it will be replicated into all instances created after that storing. It turned out we couldn’t use that because we create our instances as a group, so we needed them before we could create the table of them.
That even sounds weird, doesn’t it. But the upshot is that we have found a decent way to hold on to objects that might otherwise have to be global: hold them in the class itself.
We could also have held the shields collection in the GameRunner, which is an approach we’ve used before, where we limit the number of globals by having one master object that holds a number of others. Our SoundPlayer is an example of that in Invaders, I believe.
So things went smoothly today, except for the Apple - Working Copy conflict, which I’m sure will soon be resolved.
Next time, maybe we’ll get past Main into a new tab. Or build a new feature. See you then!