Let’s do more on the two-player game mode. We’re probably getting to the hard part.

It has been almost 24 hours since I worked on the two-player stuff, so I’d better review what we have and what’s needed.

I seem to recall that the first gunner that appears is for player 2. One clever fix might be to check the player name, and if it is 1, print 2 and vice versa. But no, that won’t do. Let’s see how this thing works:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Runner = nil
    Player1 = GameRunner(3, "1")
    Player2 = GameRunner(3, "2")
end

function startGame()
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
end

function nextPlayersTurn()
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
    Runner:requestSpawn()
end

function touched(touch)
    if Runner and Runner:livesRemaining() ~= 0 then
        Gunner:instance():touched(touch)
    else
        if touch.state == ENDED then
            testsVisible(false)
            startGame()
        end
    end
end

It also seems like if I reversed the definitions of Player1 and Player2 in setup, it would work. But something is going on between startGame and nextPlayersTurn, is my guess. What would happen if we didn’t do the reverse in startGame, or if we removed it and just had nextPlayersTurn?

I think the startGame is actually supposed to start the attract mode game running. That has stopped working. Yesterday, startGame looked like this:

function startGame(numberOfLives)
    Runner = GameRunner(numberOfLives)
    invaderNumber = 1
end

And it was called with startGame(3). So I don’t think that’s what’s causing the attract mode not to start. Ah. That happened in setup!

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    Runner = nil
    startGame(0)
end

Let’s not worry about that right now, it’s surely not affecting the player 1 player 2 thing. I’m sure that if I remove one of those reversals, things would be fine. But what if we started a game with the players reversed? We really ought to straighten them out explicitly in startGame. Let’s do that:

OK, I freely grant that this is a guess. I’m honestly not sure of the flow, but I want to try this:

function setup()
    runTests()
    parameter.boolean("Cheat", false)
    initPlayers()
end

function initPlayers()
    Runner = nil
    Player1 = GameRunner(3, "1")
    Player2 = GameRunner(3, "2")
end

function startGame()
    initPlayers()
end

function nextPlayersTurn()
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
    Runner:requestSpawn()
end

Right, well, now nothing ever happens. Let’s revert now, before it’s too late, and get some more information.

OK, back to ground zero. Let me try another experiment:

function startGame()
    Runner = Player1
    --Player1,Player2 = Player2, Player1
    invaderNumber = 1
end

That actually works as intended.

You should be asking yourself Why didn’t he just figure out what was going on instead of bashing at it like that??? And you’d be right to do so. I knew I wasn’t sure what the code did, but I changed it anyway. Surely that is a seriously stupid thing to do.

Well, in my defense, you’re right, but I never said I would never do stupid things. I guess I wanted to test my existing mental model, incomplete though it surely was and is. It might have been better–probably would have been–surely would have been. I did what I did.

Let’s look at the code now.

The startGame function changes Runner from its initial nil to an actual GameRunner, namely Player1. And it does not reverse the players, so Player1 is still, well, player “1”. Then we get to draw:

function draw()
    pushMatrix()
    pushStyle()
    background(40, 40, 50)
    showTests()
    if Runner then Runner:draw() end
    if not Runner or Runner:livesRemaining() == 0 then
        drawTouchToStart()
    end
    popStyle()
    popMatrix()
    if Runner then Runner:update() end
end

So, because we have a Runner, we’ll draw it and we’ll update it. What is it likely to do?

It will draw the current instance of Gunner. (We should think about having GameRunner own the Gunner now, things seem to be trending that way.)

The current instance of Gunner will be fresh, initialized this way:

function Gunner:init(pos)
    self:setPos(pos)
    self.zombie = false
    self.count = 210 -- count down to first player
    self.missile = Missile()
    self.gunMove = vec2(0,0)
    self.explosions = { readImage(asset.playx1), readImage(asset.playx2) }
    self.missileCount = 0
    self.alive = false
    self.drawingStrategy = self.drawNothing
    --Player.instance = self
end

It’s not alive, it draws nothing. This is consistent with our experience: initially we see no gunner on the screen. That commented out line is a leftover. Remove it.

What about GameRunner:update?

function GameRunner:update60ths()
    self.time120 = self.time120 + (DeltaTime < 0.016 and 1 or 2)
    if self.time120 < 2 then return end
    self.time120 = 0
    Gunner:instance():update()
    self.army:update()
    if self.weaponsTime > 0 and ElapsedTime >= self.weaponsTime then
        self.army:weaponsFree()
        self.weaponsTime = 0
    end
end

Main point of interest: it updates the Gunner:

function Gunner:update()
    self.missile:update()
    self:manageMotion()
    self:manageCount()
end

The main point of interest here:

function Gunner:manageCount()
    if self.count <= 0 then return end
    self.count = self.count - 1
    self:manageExplosion()
    self:manageSpawning()
end

function Gunner:manageSpawning()
    if self.count <= 0 then
        Runner:gunnerDead()
    end
end

What was count?

function Gunner:init(pos)
    self:setPos(pos)
    self.zombie = false
    self.count = 210 -- count down to first player
    self.missile = Missile()
    self.gunMove = vec2(0,0)
    self.explosions = { readImage(asset.playx1), readImage(asset.playx2) }
    self.missileCount = 0
    self.alive = false
    self.drawingStrategy = self.drawNothing
end

It was 210, so after 210 ticks, the non-alive non-drawing Gunner tells the Runner gunnerDead. Then …

function GameRunner:gunnerDead()
    nextPlayersTurn()
end

And then:

function nextPlayersTurn()
    Runner = Player1
    Player1,Player2 = Player2, Player1
    invaderNumber = 1
    Runner:requestSpawn()
end

And we set Runner to Player1 (again, because we didn’t swap yet), we swap, we set invaderNumber, we ask the runner to spawn:

function GameRunner:requestSpawn()
    Shield.shields = self.shields
    self.lm:next()(self)
end

We set the shields to be our saved ones, and call for the next life. And the beat goes on.

So I think our commented out swap is righteous. However the startup is kind of intricate isn’t it? Let’s delete the commented line, commit: in two player game player 1 goes first, then we’ll think about that concern.

Is this too complex?

Startup looks like this, if I have it right:

  1. Create two players, which create two armies, two sets of shields, and a Gunner instance (plus one that is thrown away).
  2. Upon touch, set Runner non-nil, triggering draw and update in Runner.
  3. Runner draws things including a non-drawing Gunner.
  4. Runner updates things including a non-alive Gunner who is counting down from 210. This continues until
  5. Gunner counts down, tells Runner gunner is dead.
  6. Runner asks Main to start the next Player’s turn.
  7. Main sets Runner to Player1 and swaps the players.
  8. Main tells Runner to request a spawn.
  9. Runner gets a spawn command from its LifeManager and
  10. Runner then spawns a real player or zombie as needed.

A more common way to manage this sort of thing would be for someone, probably Main or GameRunner, to be told that this is the start of a game, at which point it would directly initialize everyone explicitly, setting values all the way down.

But the same things need to happen. We want the game to wait its count of 210 before spawning a live gunner. We want it to run out of lives and finally spawn a zombie to serve as the attract mode.

There’s a design issue here that I’d like to call out for consideration.

I’m thinking of the various objects, GameRunner, Shields, Gunner, and so on, as autonomous. We do have to have some centralized control over when they draw and update, because that’s the way Codea works. But in principle, I would like it if they all just did their own thing. The Missile would fly until it either burned out or hit something. If it hit something, it would kill that thing. If that thing died, it would report its score. And so on.

Another way of coding all that would be to have a Master Control Program that knew everything about everyone, and moved them around like pieces on a game board. That’s not an uncommon way to do things, but it is not my preferred way.

However, my preferred way does make the flow of the game harder to follow. And mostly, I prefer not to try to follow it. Instead, I try to think about a single object at a time, and whether it does what it should when things happen.

Think about the Gunner, from the Gunner point of view.

  1. Initialize as not alive, count as 210
  2. When drawn, draw nothing
  3. When updated, count down the count.
  4. When count is gone, tell GameRunner you’re dead.
  5. (Later) when told to spawn, set alive (or zombie) and
  6. When drawn, draw yourself
  7. When updated, update yourself.
  8. When killed, tell Runner you’re exploding, set your count so that
  9. When drawn you explode for a while, then
  10. Report dead again, see (4 and 5 above).

It seems to me to make a lot of sense that way, and I hope it does to you. My experience with the Master Control Program style is that it is harder to write and harder to maintain, because you have to maintain state from various levels of the game. In the style we’re using here, more of the state is isolated to the objects that need and manage that state.

So, to answer the question, yes it is complex, and no, for me, it isn’t too complex. Or at least not by much: of course I’d like it to be simpler.

Now I think we have the game playing in legitimate two player mode now. I’ve played both sides all the way through, and it drops into attract mode after both players have had their three gunners.

We could release this right now as “Two-Player (Only)”. But I’d like to make two player mode an option. And I’d like the attract mode to run at the beginning.

It seems to me that for one-player mode, all we need to do is set Player2 equal to Player1 and let it go. I’ll test that idea now with a patch:

function startGame()
    Runner = Player1
    Player2 = Player1 -- patch
    invaderNumber = 1
end

That works as anticipated. Note that had I done that yesterday, I could have shipped the game with two player mode partially implemented. Now, of course we need to control which we get. I can think of two ways.

The easy way is a parameter switch for 1 or 2 players, defaulted to 1. We’d have to fiddle a bit to decide where to read it out, but in fact the switches are not a good idea and we should probably change the program not to display them except on request. Our users have commented that they just close them anyway.

It’ll be better to have two places to touch to start, for one player and for two. How about this: Instead of centered “Touch to Start”, we’ll make two touches, one on each side of the screen, saying “One Player” and “Two Players”. We’ll see if our users are smart enough to realize they have to touch them. Or, maybe “Touch for One Player”, stacked into two rows? Yes that’s nicer.

First the display:

function drawTouchToStart()
    pushStyle()
    stroke(255)
    fill(255)
    fontSize(50)
    local sc = GameRunner:scaleAndTranslationValues(WIDTH,HEIGHT)
    local y = (Constant:saucerY() + 0x10)*sc
    text("Touch to Start", WIDTH/2, y)
end

This code tells me that we are displaying in textMode(CENTER). I think I’d rather use CORNER, which will require a fudge to the y coordinate. First, I’ll just do this:

k~~~lua function drawTouchToStart() pushStyle() stroke(255) fill(255) fontSize(50) textMode(CORNER) local sc = GameRunner:scaleAndTranslationValues(WIDTH,HEIGHT) local y = (Constant:saucerY() + 0x10)*sc text(“Touch for\nOnePlayer”, 1, y) end ~~~

That immediately shows me the error of my ways, because the screen is not scaled, so this is left justified on the whole screen. That’s outside the game area and that’s not good. Let’s save the matrix and let GameRunner scale and translate for us. That’s a class method so we can call it.

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    fill(255)
    fontSize(50)
    textMode(CORNER)
    GameRunner:scaleAndTranslate()
    local y = (Constant:saucerY() + 0x10)
    text("Touch for\nOnePlayer", 1, y)
    popStyle()
    popMatrix()
end

I noticed that I didn’t pop style before. We were lucky to get away with that. Now I expect our text will be way too big. We’ll see.

too big

Yeah. I’ll look to see what font size we’re using for scoring. 10. That’s OK but too high, as we expected, and too far to the left. Let’s align with the Line, which starts at coordinate 8. I don’t quite like that, so I settle on 16:

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    GameRunner:scaleAndTranslate()
    local y = Constant:saucerY()
    text("Touch for\nOne Player", 16, y)
    popStyle()
    popMatrix()
end

That looks good to me. Let’s do the other one. That will require some fiddling. No, by golly, I did it right! A miracle!

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    GameRunner:scaleAndTranslate()
    local y = Constant:saucerY()
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
    popStyle()
    popMatrix()
end

touch two

Now for the minor detail of making it work. My cunning plan is to accept any touch left of center as starting the one player game, and any touch to right of center as starting the two player game. Therefore:

function touched(touch)
    if Runner and Runner:livesRemaining() ~= 0 then
        Gunner:instance():touched(touch)
    else
        if touch.state == ENDED then
            testsVisible(false)
            startGame(touch)
        end
    end
end

function startGame(touch)
    Runner = Player1
    if touch.x < WIDTH/2 then
        Player2 = Player1
    end
    invaderNumber = 1
end

And that does the trick. Let’s get fancy, but first I’d better commit: player can choose one or two player mode.

I think we should put boxes around those two text things. This got a bit fiddly, but here it is:

function drawTouchToStart()
    pushMatrix()
    pushStyle()
    stroke(255)
    strokeWidth(1)
    fill(255)
    fontSize(10)
    textMode(CORNER)
    rectMode(CORNER)
    GameRunner:scaleAndTranslate()
    local s1 = "Touch for\nOne Player"
    local s2 = "Touch for \nTwo Players"
    local y = Constant:saucerY() - 4
    local sx,sy = textSize(s1)
    fill(0)
    rect(8,y-4, 16+sx, sy+8)
    sx,sy = textSize(s2)
    rect(208-sx, y-4, 16+sx, sy+8)
    fill(255)
    text("Touch for\nOne Player", 16, y)
    x = 216-textSize("Two Players")
    text("Touch for\nTwo Players", x, y)
    popStyle()
    popMatrix()
end

We have some duplication there, and a serious lack of obviousness, but we do get the effect I wanted:

buttons

Commit: one and two player touch buttons.

This code needs cleaning up, but I’m out of time for the morning, and we’re shippable, so let’s sum up.

Summing Up

We fixed the bug where player 2 went first, we implemented the ability to choose a one or two player game, and we created two somewhat attractive buttons to press. The tests are green, and none of the changes were substantial. The biggest code change was in drawing the rectangles around the text.

In two short sessions, we’ve installed a substantial change which I had not intended to make, the two-player game. It has gone very smoothly, with few if any long periods of confusion. We had a total of six commits, three each day, including one that was just a renaming of a class. (That one caused the most trouble, due to the lack of a rename refactoring in Codea, requiring me to search and destroy all the references. As a human, I missed some.)

This went pretty much as I think things should go. Our functionality is divided rather well among classes, and the classes interact in just a few ways. That should mean than most changes will affect only a very few files.

Yesterday, we affected at most four files, one of which was Sound, for some reason. Today we only affected one.

This is a good result, and the kind of result I expect from decently-factored code. I emphasize decently. This code is far from perfect, more somewhere in the range of “pretty good”. For personal reasons, I hope it’s a bit past “mediocre”. Be that as it may, it was good enough to allow a quick and solid implementation of an unexpected new feature.

Life is good. Now for a small trip to the store and lunch. See you next time!

Invaders.zip