Space Invaders 72
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:
- Create two players, which create two armies, two sets of shields, and a Gunner instance (plus one that is thrown away).
- Upon touch, set Runner non-nil, triggering draw and update in Runner.
- Runner draws things including a non-drawing Gunner.
- Runner updates things including a non-alive Gunner who is counting down from 210. This continues until
- Gunner counts down, tells Runner gunner is dead.
- Runner asks Main to start the next Player’s turn.
- Main sets Runner to Player1 and swaps the players.
- Main tells Runner to request a spawn.
- Runner gets a spawn command from its LifeManager and
- 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.
- Initialize as not alive, count as 210
- When drawn, draw nothing
- When updated, count down the count.
- When count is gone, tell GameRunner you’re dead.
- (Later) when told to spawn, set alive (or zombie) and
- When drawn, draw yourself
- When updated, update yourself.
- When killed, tell Runner you’re exploding, set your count so that
- When drawn you explode for a while, then
- 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.
~~~lua
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.
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
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:
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!