Space Invaders 43
< A couple of small changes to CodeaUnit and my CUBase template. Then let’s see what we can improve about Space Invaders.
Saturday: There’s a defect here. I’ll explain in the next article. Zip file removed.
I’ll spare you the details, but I made a few tiny improvements to my CUBase template for setting up projects with CodeaUnit, and one associated change to CodeaUnit itself:
- CodeaUnit
expect
function now accepts an optional message; runTests
andshowTests
renamed and moved into the Test tab- Main tab simplified down to a minimum.
I’ll include a zipped CUBase and CodeaUnit just on the off chance that anyone out there is actually following along with actual code.
Just for fun, I’ll add a message to one of my long patches of expectations, since that’s why I added the feature, and make it fail:
_:test("finding bottom-most invader", function()
local army = Army()
local y
y = army:lowestInvaderY(1)
_:expect(y).is(105)
army:directKillInvader(1)
_:expect(army:lowestInvaderY(1)).is(121)
_:expect(army:lowestInvaderY(2), "should be 105").is(107)
army:directKillInvader(1 + 11)
_:expect(army:lowestInvaderY(1)).is(137)
army:directKillInvader(1 + 2*11)
_:expect(army:lowestInvaderY(1)).is(153)
army:directKillInvader(1 + 3*11)
_:expect(army:lowestInvaderY(1)).is(169)
army:directKillInvader(1 + 4*11)
_:expect(army:lowestInvaderY(1)).is(nil)
end)
Sure enough, it fails:
You can just about see it in the fine print there on the left:
20: finding bottom-most invader should be 105 -- Actual: 105.0, Expected: 107
Nothing fancy, but enough to do the job.
OK …
What shall we do today?
I think that today we should browse the code and look for things that could be better. One thing in particular comes to mind: I’ve taken recently to creating methods where I would previously have accessed an attribute, even when the method really does return a constant.
There are some advantages to this approach. First, if we did it uniformly, then there’d only be one option for talking to any object, sending it a message, and we wouldn’t have to remember whether this one was one of the message ones or the attribute ones. They’d all be message ones.
Second, when we export an attribute, we’re stuck with it. We’re less stuck with a method. Yes, we need to retain the name and meaning, but we can implement that meaning in any fashion we choose. We gain some flexibility, and from a programming viewpoint, it’s free. We’re not coding in some great general thing. We just always call methods and they always do what we need, however it’s done.
There is one drawback: a method is a function call. It does the same fetch from the object’s table as an attribute access, but then it calls what comes back instead of returning it. So a method call is slower than an attribute access.
I choose to entirely ignore this concern. If the program ever needs to be faster, we probably won’t get that speed by switching to accessors, we’ll more likely get it by using a better algorithm, and the method approach actually makes changing the algorithm easier.
Plus, if you’re still not bought in, come back and tell me the speed of a Codea attribute access versus a function call. If, after seeing those two tiny numbers, you still care.
So we’ll be looking for attribute accesses.
Another issue is globals. Our tests have given us problems because there are a fair number of global objects around, and random people are accessing them, sometimes to our detriment. We’‘ll stay alert for them and see whether we can hide most of them. Quite likely the GameRunner will be a good place for some, and possibly Army will be another good hiding place. We’ll see.
Finally, we have mostly moved to objects over global tables with global functions. I believe the outstanding exception is Gunner. Having thought of it, I choose that as the first thing to deal with. Here’s Gunner tab:
-- Gunner
-- RJ 20200819
function touched(touch)
local fireTouch = 1171
local moveLeft = 97
local moveRight = 195
local moveStep = 1.0
local x = touch.pos.x
if touch.state == ENDED then
GunMove = vec2(0,0)
if x > fireTouch then
fireMissile()
end
end
if touch.state == BEGAN or touch.state == CHANGED then
if x < moveLeft then
GunMove = vec2(-moveStep,0)
elseif x > moveLeft and x < moveRight then
GunMove = vec2(moveStep,0)
end
end
end
function explode()
Gunner.alive = false
Gunner.count = 240
SoundPlayer:play("explosion")
end
function setupGunner()
local missile = Missile()
Gunner = {pos=vec2(104,32),alive=true,count=0,explode=explode,missile=missile}
GunMove = vec2(0,0)
GunnerEx1 = readImage(asset.playx1)
GunnerEx2 = readImage(asset.playx2)
end
function drawGunner()
pushMatrix()
pushStyle()
Gunner.missile:draw()
tint(0,255,0)
if Gunner.alive then
sprite(asset.play,Gunner.pos.x,Gunner.pos.y)
else
if Gunner.count > 210 then
local c = Gunner.count//8
if c%2 == 0 then
sprite(GunnerEx1, Gunner.pos.x, Gunner.pos.y)
else
sprite(GunnerEx2, Gunner.pos.x, Gunner.pos.y)
end
end
Gunner.count = Gunner.count - 1
if Gunner.count <= 0 then
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
Gunner.alive = true
end
end
end
end
popStyle()
popMatrix()
end
function updateGunner()
Gunner.missile:update()
if Gunner.alive then
Gunner.pos = Gunner.pos + GunMove + vec2(effectOfGravity(),0)
Gunner.pos.x = math.max(math.min(Gunner.pos.x,208),0)
end
end
function effectOfGravity()
local effect = 0.1
local gx = Gravity.x
return gx > effect and 1 or gx < -effect and -1 or 0
end
function fireMissile()
if not Gunner.alive then return end
local missile = Gunner.missile
if missile.v == 0 then
missile.pos = Gunner.pos + vec2(7,5)
missile.v = 1
SoundPlayer:play("shoot")
end
end
Let’s go ahead and convert this thing to an object. We’ll have to look for callers of each of those functions, but that aside, and converting Gunner
to self
a lot, this should be straightforward. (cf. “Hold my beer.”)
One issue is that presently we use Gunner
as the global name of the instance. That tempts me to name the class Player, which is more the Space Invaders thing to do. Yeah, let’s do that, and we’ll of course rename the tab as well.
I naively convert this tab to an object:
-- Player
-- RJ 20200819
-- made Player class 20200911
Player = class()
function Player:init(pos)
self.pos = pos or vec2(104,32)
self.alive = true
self.count = 0
self.missile = Missile()
self.gunMove = vec2(0,0)
self.ex1 = readImage(asset.playx1)
self.ex2 = readImage(asset.playx2)
end
function Player:touched(touch)
local fireTouch = 1171
local moveLeft = 97
local moveRight = 195
local moveStep = 1.0
local x = touch.pos.x
if touch.state == ENDED then
self.gunMove = vec2(0,0)
if x > fireTouch then
self:fireMissile()
end
end
if touch.state == BEGAN or touch.state == CHANGED then
if x < moveLeft then
self.gunMove = vec2(-moveStep,0)
elseif x > moveLeft and x < moveRight then
self.gunMove = vec2(moveStep,0)
end
end
end
function Player:explode()
self.alive = false
self.count = 240
SoundPlayer:play("explosion")
end
function Player:draw()
pushMatrix()
pushStyle()
self.missile:draw()
tint(0,255,0)
if self.alive then
sprite(asset.play, self.pos.x, self.pos.y)
else
if self.count > 210 then
local c = self.count//8
if c%2 == 0 then
sprite(self.ex1, self.pos.x, self.pos.y)
else
sprite(self.ex2, self.pos.x, self.pos.y)
end
end
self.count = self.count - 1
if self.count <= 0 then
if Lives > 0 then
Lives = Lives -1
if Lives > 0 then
self.alive = true
end
end
end
end
popStyle()
popMatrix()
end
function Player:update()
self.missile:update()
if self.alive then
self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
self.pos.x = math.max(math.min(self.pos.x,208),0)
end
end
function Player:effectOfGravity()
local effect = 0.1
local gx = Gravity.x
return gx > effect and 1 or gx < -effect and -1 or 0
end
function Player:fireMissile()
if not self.alive then return end
if self.missile.v == 0 then
self.missile.pos = self.pos + vec2(7,5)
self.missile.v = 1
SoundPlayer:play("shoot")
end
end
Now I’ll go find all the references to Gunner and do something about them.
In Main, I just create a player called Gunner. That seems likely to be useful:
function setup()
Runner = GameRunner()
runTests()
SoundPlayer = Sound()
createShields()
createBombTypes()
Gunner = Player() -- <---
invaderNumber = 1
Lives = 3
Score = 0
Line = image(208,1)
for x = 1,208 do
Line:set(x,1,255, 255, 255)
end
end
I also need a touched
function now, because I converted the Player one to a method.
function touched(touch)
Gunner:touched(touch)
end
GameRunner draws the Gunner:
function GameRunner:draw()
pushMatrix()
pushStyle()
noSmooth()
rectMode(CORNER)
spriteMode(CORNER)
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate(WIDTH/8-112,0)
TheArmy:draw()
Gunner:draw() -- <---
drawShields()
drawStatus()
popStyle()
popMatrix()
end
But, oddly, the Army updates it:
function Army:update()
Gunner:update()
self:updateBombCycle()
self:possiblyDropBomb()
local continue = true
while(continue) do
continue = self:nextInvader():update(self.motion, self)
end
self.rollingBomb:update(self)
self.plungerBomb:update(self)
self.squiggleBomb:update(self)
end
The Army also references the Gunner’s position. For now we’ll allow it:
function Army:targetedColumn()
local gunnerX = Gunner.pos.x -- <---
local armyX = self:xPosition()
local relativeX = gunnerX - armyX
local result = 1 + relativeX//16
return math.max(math.min(result,11),1)
end
Yes, I’d like that to be a method, as discussed above. But right now, we’re refactoring, not changing implementation, so that would be a diversion. We’re smart enough to pull it off, but also smart enough not to try.
Bombs can kill the gunner:
function Bomb:killsGunner()
if not Gunner.alive then return false end
local hit = rectanglesIntersectAt(self.pos,3,4, Gunner.pos,16,8)
if hit == nil then return false end
Gunner:explode()
return true
end
Here again, we think this will work and we’re not going to quibble over the attribute access.
The rectangles stuff bugs me and I think the objects should be asked for their collision information and provide it. Again, that’s not for just now.
The tests have a number of references, and I think I’ll let them tell me what needs changing by running them. I think I’ve done all I can find by hand. Let’s see what happens.
Delightfully, the game runs. And only two tests fail:
9: Bomb hits gunner -- Tests:72: attempt to call a nil value (global 'setupGunner')
19: rolling shot finds correct column -- Tests:188: attempt to call a nil value (global 'setupGunner')
With some luck, we can replace those with
Gunner = Player()
And back away slowly.
Yiss! All the tests pass and the game runs.
I think we’ll commit at this point: Gunner = Player() class instance.
Now how about getting rid of that global. It seems to me that the game runner should own the player, and pretty much everything else. We’ll try that and see what happens.
We remove the definition of the Gunner global from Main. In GameRunner:
function GameRunner:init()
TheArmy = Army()
self.player = Player()
self:resetTimeToUpdate()
end
I see another global there to go after. One thing at a time, Ron, one thing at a time.
In draw:
function GameRunner:draw()
pushMatrix()
pushStyle()
noSmooth()
rectMode(CORNER)
spriteMode(CORNER)
stroke(255)
fill(255)
scale(4) -- makes the screen 1366/4 x 1024/4
translate(WIDTH/8-112,0)
TheArmy:draw()
self.player:draw()
drawShields()
drawStatus()
popStyle()
popMatrix()
end
We noticed that Gunner got updated in Army: let’s move that up into GameRunner:
function GameRunner:update60ths()
self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
if self.time120 < 2 then return end
self.time120 = 0
self.player:update()
TheArmy:update()
end
Army has a number of references to the Gunner
global that we need to replace somehow. How about if we pass the gunner to the army when we create him, since he needs it.
function Army:init(player)
self.player = player
local vader11 = readImage(asset.inv11)
...
function Army:targetedColumn()
local gunnerX = self.player.pos.x
local armyX = self:xPosition()
local relativeX = gunnerX - armyX
local result = 1 + relativeX//16
return math.max(math.min(result,11),1)
end
The bombs know the gunner as well. They’re pretty far dow in the stack of objects, and they already have a lot of parameters with their shapes and explosions and such. For now, we’ll let them ask the GameRunner, which may wind up being nearly our only necessary global, unless we want to pass it around everywhere. (Which could be the right thing, even if I’m not going to do it.)
function Bomb:killsGunner()
if not Runner.player.alive then return false end
local hit = rectanglesIntersectAt(self.pos,3,4, Runner.player.pos,16,8)
if hit == nil then return false end
Runner.player:explode()
return true
end
There’s more than a little feature envy there. We may revist that one day.
The tests both make a Gunner, but they do it as a global. That’s troubling and could make weird stuff happen now that we don’t really want a global of that name. We’ll try making it a local to the tests first.
A bunch of tests fail. We’ll chase those shortly. Also an error as the game runs:
Army:112: attempt to index a nil value (field 'player')
stack traceback:
Army:112: in function <Army:111>
(...tail calls...)
Army:88: in method 'dropBomb'
Army:59: in method 'dropRandomBomb'
Army:48: in method 'possiblyDropBomb'
Army:125: in method 'update'
GameRunner:44: in method 'update60ths'
GameRunner:32: in method 'update'
Main:31: in function 'draw'
That looks like a problem in at least one test as well. The issue is that Army expects to be handed a Player and isn’t.
function GameRunner:init()
self.player = Player()
TheArmy = Army(self.player)
self:resetTimeToUpdate()
end
This will be true for any other creators of Army instances, such as in the tests. Some of the tests won’t care, others will. I’ll let the tests tell me.
19: rolling shot finds correct column -- Army:112: attempt to index a nil value (field 'player')
_:test("rolling shot finds correct column", function()
local army = Army()
local Gunner = Player()
local bomb = army.rollingBomb
...
Needs to be:
_:test("rolling shot finds correct column", function()
local Gunner = Player()
local army = Army(Gunner)
local bomb = army.rollingBomb
...
9: Bomb hits gunner – Actual: false, Expected: true
This one may need more work.
~~~lua
_:test("Bomb hits gunner", function()
local Gunner = Player()
Gunner.pos=vec2(50,50)
-- gunner's rectangle is 16x8 on CORNER
-- covers x = 50-65 y = 50,57
local bomb = Bomb(vec2(50,50))
-- bomb is 3x4, covers x = 50-52 y = 50-53
_:expect(bomb:killsGunner()).is(true)
bomb.pos.y = 58
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
bomb.pos.y = 57
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 48 -- covers x = 48,49,50
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 47 -- 47,48,49
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
bomb.pos.x = 65 -- 65,66,67
Gunner.alive = true
_:expect(bomb:killsGunner()).is(true)
bomb.pos.x = 66 -- 66,67,68
Gunner.alive = true
_:expect(bomb:killsGunner()).is(false)
end)
Well. Bomb:killsGunner needs a live player in Runner. So we’ll need to create at least a GameRunner here.
_:test("Bomb hits gunner", function()
Runner = GameRunner()
local Gunner = Runner.player
Gunner.pos=vec2(50,50)
...
I’m hopeful but not certain about that. We’ll see. Ha! Tests all pass. Commit: remove Gunner global, player in GameRunner.
We could probably save some setup in the tests by moving some things to before
and after
, but that’s for another day. Ideally, we’d use several different setups, but one might suffice. Be that as it may, we’re done for now, except for the summing up.
Summing Up
A fairly simple process of creating a class, using it directly “all over”, then centralizing its definition into a single class (GameRunner) has resulted in removal of a global variable and a bit of simplification in the code.
A good morning’s work.
The tests carried some weight today, and did it nicely. They did find some problems for us in the actual code, and the issues with changing them over to the new scheme were fairly minimal.
It’s worth pointing out that the issues with the tests revolved around two main things. First, we had a global and used it a lot in the tests. Second, We didn’t centralize the creation of that global within the tests. We could have had a better test design without much trouble and that would have assisted in the process by requiring fewer edits to get all the tests running again.
As for the first issue, the very existence of the globals, we should stay more alert to that. Globals mess up the code, making surprise connections among objects that will come back to bite us. This shows up often in the tests, which makes them harder to maintain, which induces us to write fewer tests and remove them when they irritate us.
However, it’s not at all clear at the beginning where something should go, and a global finesses that decision, deferring it into the future. But it also guarantees work in the future. We’re not deeply troubled by that: we are here to harvest the value of change, not to avoid it.
Except that by programming in certain ways, we may be able to keep our rapid progress, have even more flexibility for future change harvesting, and make the inevitable rework less arduous. How might we do that?
Well, what if instead of defining global variables, we defined global functions that returned the object that’s needed? Then we could change anything underneath that function. We could even create a whole different kind of object to handle things internally, and return an adapter to folks who called our “legacy global”.
It might be worth trying that for a while, to see if it’s helpful. My intuition says that it might be. That’s enough to get me to try it, because it’s clear that it can’t be much worse:
function Gunner()
return TheGunner
end
That’s so close to having the global that it’s clearly no more costly to do, nor more costly to move around, and it gives us this kind of flexibility all over:
function Gunner()
return Runner.player
end
Hm. And shouldn’t that be Runner:player()
as well, getting rid of those accessors that I was complaining about?
Perhaps it should. But that’s for another day!
See you then!
* Saturday: Zip removed.*