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
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
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.
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.
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.
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!