Space Invaders 65
Improving zombie behavior. Sound adjustments. Maybe more. Maybe less.
Dave1707 from the Codea forum tried yesterday’s version–I’m glad someone is–and reported that the sound during attract mode was irritating, especially since the saucers basically fly all the time. So I plan a quick change for that. He also noticed that the zombie player can run off the screen, which I observed yesterday but didn’t take the time to fix. We’ll fix that as well.
We may also have to try to fix the 6 key on my Mac’s Magic Keyboard, or borrow my wife’s. A new one should arrive tomorrow. Keyboard, not wife.
Let’s see about the sound. There is a volume parameter in the call to sound
, so let’s add a volume member and use it. Sound presently looks like this:
function Sound:init(noisy)
self.noisy = true
self:defineSounds()
self:defineOminousTones()
end
function Sound:beSilent()
self.noisy = false
end
function Sound:beNoisy()
self.noisy = true
end
The noisy
flag is used by the tests to keep them quiet. We can use volume, which ranges from zero to one, for that purpose, so let’s do this:
function Sound:init(noisy)
self.volume = 1
self:defineSounds()
self:defineOminousTones()
end
function Sound:setVolume(vol)
self.volume = vol
end
function Sound:beSilent()
self:setVolume(0)
end
function Sound:beNoisy()
self:setVolume(1)
end
Now let’s use the volume:
function Sound:play(aSoundName)
self:playRaw(self.sounds[aSoundName])
end
function Sound:playRaw(aSound)
sound(aSound, self.volume)
end
I expect everything to be the same at this point, and it is. Now let’s see if we can find a place to set the volume lower during zombie mode. Here’s one possibility:
function Player:spawn(zombie, pos)
self.zombie = zombie or false
self.zombieCount = 0
self.zombieMotion = 0
self.alive = not self.zombie
self.drawingStrategy = self.drawPlayer
self:setPos(pos)
end
We could check the zombie flag here and adjust the sound. But that seems kind of deep in the system to be making that global a decision. Let’s see how we call this method.
function GameRunner:gameOver()
self:spawn(true)
end
This looks like a good place to turn it off. How does the first player start up?
The player sends requestSpawn
to the GameRunner, which is:
function GameRunner:requestSpawn()
self.lm:next()(self)
end
Life manager is initialized with these operations:
self.lm = LifeManager(numberOfLives or 3, self.spawn, self.gameOver)
So we’ll call ‘spawn’ when the game is on, and otherwise gameOver
. Unfortunately, right now gameOver
calls spawn
also, so we don’t quite have a place to put the sound call. I think we’ll let it reside in the Player for now, but I don’t feel perfect about it.
function Player:spawn(zombie, pos)
self.zombie = zombie or false
if self.zombie then
Runner:soundPlayer():setVolume(0.05)
else
Runner:soundPlayer():setVolume(1.0)
end
self.zombieCount = 0
self.zombieMotion = 0
self.alive = not self.zombie
self.drawingStrategy = self.drawPlayer
self:setPos(pos)
end
It turns out that anything bigger than 0.05 is loud enough to be irritating in attract mode.
There is one other issue, which is that until the first player spawns in attract mode, the sound is set at 1. For now, I’ll let that ride. I’m semi-planning, which is slightly better than just hoping, that we’ll move knowledge of attract mode a bit higher up and we can deal with the sound then.
We’ll commit: sound at 0.05 during attract mode.
I’m getting a bit of sound during the tests. Let’s see what they do.
_:before(function()
Runner = GameRunner(3)
Runner:soundPlayer():beSilent()
end)
_:after(function()
Runner:soundPlayer():beNoisy()
Runner = nil
Player:clearInstance()
end)
That looks good to me. Does someone else create a GameRunner in here? There’s one created on the fly but it’s not likely to make any noise:
_: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)
When we were using the noisy
flag, there was no sound from the tests. This is a bit mysterious. I’m going to ignore it for now and move on. This feels a bit bad but my energy to chase this has dissipated. Sometimes I need a pair to pick up the slack.
Keep Zombies Penned Up
The zombie players can run off screen. I looked at this last night and the reason is pretty clear:
function Player:manageMotion()
if self.alive then
self:manageNormalMotion()
elseif self.zombie then
self:manageZombieMotion()
end
end
function Player:manageNormalMotion()
self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
self.pos.x = math.max(math.min(self.pos.x,208),0)
end
function Player:manageZombieMotion()
if math.random(10) > 5 then self:fireMissile() end
if self.zombieCount > 0 then
self.zombieCount = self.zombieCount - 1
self.pos.x = self.pos.x + self.zombieMotion
else
self.zombieCount = math.random(10,20)
self.zombieMotion = math.random(-1,1)
end
end
The normal motion clamps the player position between 0 and 208. The zombie one does not. The quick fix without duplication is this:
function Player:manageMotion()
if self.alive then
self:manageNormalMotion()
elseif self.zombie then
self:manageZombieMotion()
end
self.pos.x = math.max(math.min(self.pos.x,208),0)
end
function Player:manageNormalMotion()
self.pos = self.pos + self.gunMove + vec2(self:effectOfGravity(),0)
end
Now it clamps for both. Testing … and that works. Commit: clamp zombies onto screen.
I noticed something else last night: bombs don’t kill zombies. I think attract mode would be more interesting if the zombie player would get destroyed and respawn.
There must be a check for bombs killing player.
function Bomb:killsGunner()
local player = Player:instance()
if not player.alive then return false end
local hit = rectanglesIntersectAt(self.pos,3,4, player.pos,16,8)
if hit == nil then return false end
player:explode()
return true
end
Sure enough, there it is. We’ll do this:
function Bomb:killsGunner()
local player = Player:instance()
if not player.alive and not player.zombie then return false end
local hit = rectanglesIntersectAt(self.pos,3,4, player.pos,16,8)
if hit == nil then return false end
player:explode()
return true
end
There’s a bit of feature envy here, with all these references to the player. We should be asking it questions, not inspecting its flags. Let’s first see that this works, however.
The zombie player does explode now, but firing keeps happening while it’s off screen.
function Player:fireMissile()
if (self.alive or self.zombie) and self.missile.v == 0 then
self:unconditionallyFireMissile()
end
end
We really want to know if it’s exploding or off screen. There’s the somewhat mysterious count
variable, that is non-zero when we’re exploding or off screen. We can check it here. I’m inclined to check it in addition to the other flags.
This is, of course, a sign that I’m not sure what the interactions are between alive, zombie, and count. In turn, that’s a sign that the code is not clear in this area. We’ll want to deal with that, but not today.
So …
function Player:fireMissile()
if (self.count == 0 and (self.alive or self.zombie)) and self.missile.v == 0 then
self:unconditionallyFireMissile()
end
end
This does the right thing in the game, but it breaks a test:
26: Player counts shots fired -- Actual: 0, Expected: 1
_:test("Player counts shots fired", function()
local Gunner = Player()
Gunner:spawn()
_:expect(Gunner:shotsFired()).is(0)
Gunner:fireMissile()
_:expect(Gunner:shotsFired()).is(1)
end)
I reckon that count
isn’t zero here. But we can call the unconditional version just as well:
_:test("Player counts shots fired", function()
local Gunner = Player()
Gunner:spawn()
_:expect(Gunner:shotsFired()).is(0)
Gunner:unconditionallyFireMissile()
_:expect(Gunner:shotsFired()).is(1)
end)
Tests are green. Commit: Zombie player explodes when hit.
Let’s sum up.
Summary
These changes went fairly smoothly, though not perfectly. There were little nitty-gritty details that had to be dealt with, changes in a few more places than seem right, and a few surprises.
This is very typical of code that has been around a while. We even ran into a place where I wasn’t really sure of just the right change, so I made a rather large one, the checking of count
in determining whether we could fire a missile.
That is a sure sign of code that isn’t communicating well, and a nearly sure sign of code that isn’t organized as well as it might.
We recognized that yesterday, when we observed that the zombie flag and the alive flag have three meaningful states and one state that has no meaning: alive and zombie. The system is in attract mode when there are no lives left and the Player requests a respawn. The respawn happens … with a zombie. That happens very low down in the code and isn’t reflected higher up.
function GameRunner:gameOver()
self:spawn(true)
end
function GameRunner:spawn(zombie)
Player:instance():spawn(zombie)
self:setWeaponsDelay()
end
Probably that parameter should be named isZombie
, don’t you think? Let’s do that.
function GameRunner:gameOver()
self:spawn(true)
end
function GameRunner:spawn(isZombie)
Player:instance():spawn(isZombie)
self:setWeaponsDelay()
end
Commit: rename isZombie parameter.
That doesn’t correct the whole program, however. We do, however, have a clear indication in GameRunner that the game is over, so there is a good prospect for cleaning this up a bit. We will leave that for another time and a clear head.
Feelings
I should mention feelings here. Not the soft gentle feeling one gets when the cat is particularly loving (or cold, you never really know with cats), but the tense feeling that one gets when the code isn’t quite right. It’s as if you’re carrying one more plate than you’re quite able to handle, or when it looks like that one screw is going to cross-thread, or when suddenly the road is incredibly slick and someone is stopped in front of you. (If you feel that badly when coding, consider reverting.)
The point is, when the code’s not right, you can feel it in your body. Maybe your neck gets tight or your stomach feels off. Maybe when the loving cat comes up you shout at it.
This tension is a sign that the code isn’t carrying the load that it should, so that your mind has to deal with more than it is comfortable dealing with. Yes, I know that the macho brogrammer can remember 16 million things without fumbling. I also know that the macho brogrammer makes more mistakes per unit time when the number of things gets above 7. Seven, not seven million.
I try to pay attention to tension and other feelings as I work. They are important clues, suggesting extra care, slowing down, more tests, even reverting and starting over.
We’re not in revert territory, but we do need some cleanup here on Aisle 5. Coming soon. See you then!