We’ll look at using invader count as a flag to start a new rack. I don’t promise to change it.
To match up with the original game’s rippling style of drawing, we only move one invader in every 60th of a second update cycle. We skip any aliens who are no longer alive. At the end of the cycle, we look to see if any aliens have reported for duty, and if not, we marshal new troops and ask for a weapons hold.
It looks like this:
function Army:cycleEnd() self:handleEmptyRack() self:startRackUpdate() end function Army:handleEmptyRack() if self.invaderCount <= 0 then self:marshalTroops() Runner:requestWeaponsHold(2) end end
Yesterday I noticed that when the invaders got to the bottom of the screen during attract mode, the game just repeatedly spawned new gunners, never calling for a new rack. The same was probably possible for real players as well.
I tried to fix that this way:
function Army:reportingForDuty(invaderPos) if invaderPos.y < 48 then Runner:forceGameOver() self.invaderCount = 0 else self.invaderCount = self.invaderCount + 1 end end
If an invader has reached below the 48th parallel, we force game over and set invader count to zero, which, my thinking was, would trigger a new rack. And it did seem to work, though I didn’t test it extensively, as it takes forever. I commented at the time:
This is a bit magical, triggering such an important notion as a new rack by setting a count but that’s how it works. This will take a while to test fully but my first test makes me think it works OK.
I think we can commit: fixed game over behavior when invaders hit bottom to marshal new troops.
Thinking about it now, not only is it too magical, I suspect it doesn’t really work. The check for the count being zero only happens at cycle end. So if there is another invader in the cycle who has NOT reached the 48th parallel, the count will not be zero and the game won’t restart yet. I’m in no way certain whether that could happen or not. It depends on the order invaders are processed. It it last to first, or first to last. Well, to tell you the truth, in all this excitement I kinda lost track myself.
Now we have told the GameRunner to force the game to be over, but that doesn’t do very much:
function GameRunner:forceGameOver() self.lm:forceGameOver() Gunner:instance():explode() end
The Life Manager just empties its array of lives, so that the next call returns a zombie, and we’re in attract mode. I think we could demand a new rack from the Army from here. There are two cases when we force game over. Either it’s over for all the players, in which case starting the zombie with a new rack will be just fine, or it’s only over for the first player of two, in which case the second player will have his own rack and not be affected.
In principle, then, we can call from here back down to Army to cause the new marshaling. We are, however, right in the middle of an invader drawing cycle. And we still have to see
invaderCount at zero to do the new marshaling, unless we do something special.
Doing something special, this late in the development of a system, is risky. There are lots of interactions going on, and even if they are all just squeaky clean, we probably can’t remember them all. And, since we are late in development, some things might not be squeaky clean. So we’d like to have this all work as directly and simply as possible–even more so than usual, and we always want that.
Let’s do this. Here:
function Army:handleEmptyRack() if self.invaderCount <= 0 then self:marshalTroops() Runner:requestWeaponsHold(2) end end
Let’s add a new Army member variable,
needsMarshaling, usually false, set to true when we need to marshal new troops no matter whether there are invaders left or not, i.e. in this situation.
We’ll init the variable to false when we create the object and when we’ve marshaled. We’ll provide a function to set the flag, and call back from Runner to set it.
function Army:handleEmptyRack() if self.needsMarshalling or self.invaderCount <= 0 then self:marshalTroops() Runner:requestWeaponsHold(2) end end function Army:marshalTroops() self.invaders = MarshallingCenter():marshalArmy(self) self.overTheEdge = false self.needsMarshaling = false end function Army:initMemberVariables() self.weaponsAreFree = false self.armySize = 55 self.invaderCount = 0 self.needsMarshaling = true 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.saucerCycle = 0 self.score = 0 end function Army:forceMarshalling() self.needsMarshaling = true end function Army:reportingForDuty(invaderPos) if invaderPos.y < 48 then Runner:forceGameOver() else self.invaderCount = self.invaderCount + 1 end end function GameRunner:forceGameOver() self.lm:forceGameOver() Gunner:instance():explode() self.army:forceMarshalling() end
I decided to set the flag to true in
initMemberVariables, since invader count is set to zero. In any case it doesn’t matter because:
function Army:init() self:initMemberVariables() self:marshalTroops() self:defineBombs() end
This should now do what I have in mind. Let me test and see if it remotely works.
I found something that doesn’t work. I’m setting full screen so often that I can’t get my console up. That won’t do, and I’m going to fix it right now.
Disappointingly, I still get the game over perpetual explosion. The troops do not marshal. I need to make this easier to test. I’ll change the 48th parallel to something higher up. However, in a review of the code:
function Army:handleEmptyRack() if self.needsMarshalling or self.invaderCount <= 0 then self:marshalTroops() Runner:requestWeaponsHold(2) end end
We seem not to agree on how to spell
marshaling. I named the center with two l’s, but Scrivener wants to spell it with one. According to random internet sites, either is correct. For consistency, I’ll rename all my occurrences to have two l’s.
Now it works. It has an interesting side effect, which is that the sound it makes is amazing. I think it’s doing two player explosions at once. I suppose it should also explode the invader. Let’s see if we can do that.
We have this invader function:
function Invader:killedBy(missile) if not self.alive then return false end if self:isHit(missile) then self.alive = false self.army:addToScore(self.score) self.exploding =15 Runner:soundPlayer():play("killed") return true else return false end end
We could refactor to pull out the exploding death bit, which should probably be done anyway, but we’d need to fiddle with the scoring. Should the Gunner get credit for destroying the invader that eats him? Tough call. I think we’ll leave this alone, but I do wonder why we’re getting that weird sound.
I think what’s happening is that when the invaders get below the parallel, we force game over, which calls for spawning what will be a zombie. Since the invaders are still down blow the parallel, that zombie dies instantly, repeat until we go through the rack.
We need to make forceNewGame kill all the invaders, or otherwise end the draw/update cycle. The latter is better if we can do it, I think, because it’s less invasive.
Where’s the invader draw and update stuff?
function Army:update() self:updateBombCycle() self:possiblyDropBomb() self:updateOneLiveInvader() self.rollingBomb:update(self) self.plungerBomb:update(self) self.squiggleBomb:update(self) self.saucer:update(self) end function Army:updateOneLiveInvader() local continue = true while(continue) do continue = self:nextInvader():update() end end function Army:nextInvader() self:handleCycleEnd() local inv = self.invaders[self.invaderNumber] self.invaderNumber = self.invaderNumber + 1 return inv end function Army:handleCycleEnd() if self.invaderNumber > #self.invaders then self:cycleEnd() end end function Army:cycleEnd() self:handleEmptyRack() self:startRackUpdate() end function Army:handleEmptyRack() if self.needsMarshalling or self.invaderCount <= 0 then self:marshalTroops() Runner:requestWeaponsHold(2) end end
Now if the
needsMarshalling flag is set, we would like to short-circuit all this. We can push it all the way up to
handleCycleEnd, this way:
function Army:handleCycleEnd() if self.needsMarshalling or self.invaderNumber > #self.invaders then self:cycleEnd() end end
I think that should make us just explode one player. I’ll test. And yes, that does the trick. I’d like to take a short morning today, so let’s commit: improved game over when invaders get to bottom.
So this got a little more gnarly than felt ideal. I never felt terribly confused, but the multiple gunner death thing, which was a surprise, shows that there’s a gap between my understanding and the actual code. As is often the case, that gap is caused by the code’s behavior being more complex than will fit in my brain.
My friend Bill Wake is doing a series of videos on state machines just now, and what we have here is the sort of a situation that a state machine could help with … if we had one.
Our game does go through a series of states that we can identify, including
- Displaying tests–or not
- Playing a zombie–or a real player
- Player dies with lives left–or not
- Army is empty–or not
- At end of invader list–or not
If we were to undertake a state-machine implementation of this, I think we’d end up with machines within machines, which is not uncommon. However, we’re not going to do that at this late date, and I’m not sure that I ever would.
My reason for that may not be a good one.
I choose to build these examples very incrementally, and to discover the need for design improvement as I go. I don’t try to do bad things, but I try never to design much ahead of what I need right in the moment. A state machine design can surely be done that way, but given that I’m very out of practice on state machines, it would require more up front, ongoing, on paper design than I choose to do in these examples.
That said, it could be a good way to go for a game like this, and the more complicated the game is, the more the state machine approach might appeal. Until it breaks down.
In my experience, a state machine design often does break down, or one’s commitment to the style breaks down, and one starts tacking things onto the side of the machine. If you’re reading “He does it wrong” here, you could be right. Quite possibly if I were good enough with a state machine design, it would hold up. I can only report that when I was as good at is as I ever got, they still tended to break down under stress.
It could be that I wasn’t good enough. Or it could be that to use them in real situations you have to be darn good at state machines.
Be that as it may, I don’t often use them. Can’t remember the last time I did, so it was probably prior to last week.
What about what we do have? It’s working now. It’s a bit odd, because to make it work well, we have to check our
needsMarshaling flag in two different places. Whenever that happens, it’s a code smell.
And yes, the fact that we check “alive” flags all over the place is a code smell. Usually what it means is that we have a bit of feature envy going on, where one bit of code is doing something that should be forwarded to the object whose flag we’re checking. Sometimes, though, there are several behaviors that should be conditioned on that flag.
In that case, quite possibly there is a new object trying to be born, a handler for that collection of behaviors. Sometimes we help it be born. Maybe we’ll look for that situation in a future article and see what we think.
For today, we’re done. The day went smoothly. There was the odd multi explosion thing but it was quickly resolved. It does identify some rough spots in the design, but they feel tolerable to me.
Overall, I wouldn’t say the game is of professional quality quite yet, but it is quite playable and as a sort of hobby example, it’s in rather good condition.
Soon, we’ll stop. Almost certainly before article 100.
See you next time!