511 lines
19 KiB
Text
511 lines
19 KiB
Text
== Lesson 12: Score, Lives, and Game Over
|
|
|
|
image::learn/12-score.png[The finished lesson, 480]
|
|
|
|
Your game keeps score, takes lives, and says GAME OVER. It is nearly a game.
|
|
Three things are still missing. It has no front door: it is in the middle of
|
|
the action the instant it loads. It forgets: quit, and the best score anybody
|
|
ever got goes with it. And it keeps track of what it is doing with two
|
|
true-or-false variables, `over` and `dying`, which is the trap lesson seven
|
|
warned you about.
|
|
|
|
This lesson fixes all three. One variable for what the game is doing, a title
|
|
screen to start from, a high score that is still there tomorrow, and a second
|
|
game that starts exactly like the first.
|
|
|
|
=== A Game Is Always in Exactly One State
|
|
|
|
Watch any arcade machine for two minutes. It shows a title and waits for
|
|
somebody. Somebody presses start and it plays. The player runs out of lives
|
|
and it says so, then waits again. The machine is always doing exactly one of
|
|
those things.
|
|
|
|
Dodge, in lesson seven, kept that in one variable holding a word: `"waiting"`,
|
|
`"playing"`, or `"over"`. Lesson nine's game went back to a true-or-false
|
|
`over`, and lesson ten added `dying` beside it. Count what two of those can
|
|
say. Each is `true` or `false`, so together they make four combinations. Three
|
|
mean something: playing, dying, and over. The fourth, both at once, is
|
|
nonsense, and your code has to be careful never to produce it. It is careful:
|
|
look at `endGame`, which sets `dying` back to `false` before it sets `over` to
|
|
`true`. Now add a title screen. That is a third flag, eight combinations, and
|
|
four of them nonsense. That is lesson seven's trap exactly, and it is where
|
|
small games go to die.
|
|
|
|
So do what Dodge did. Four states, one variable. Put the names of the states
|
|
under your other constants:
|
|
|
|
[source,lua]
|
|
----
|
|
local TITLE = "title"
|
|
local PLAYING = "playing"
|
|
local DYING = "dying"
|
|
local OVER = "over"
|
|
----
|
|
|
|
Then take `over` and `dying` out of your variables, and put one line in their
|
|
place:
|
|
|
|
[source,lua]
|
|
----
|
|
local state = TITLE
|
|
----
|
|
|
|
Dodge used the words themselves. Names in capitals are not cleverer, and they
|
|
do not catch a spelling mistake: Lua treats a misspelled name as `nil` and a
|
|
misspelled word as a word that matches nothing, and either one gives you a
|
|
game that quietly stops responding. What the names buy is a list. Every state
|
|
the game can be in is written down once, at the top of the file, where you can
|
|
see all four at a glance.
|
|
|
|
Everywhere the game used to set a flag, it now sets the state. In
|
|
`updateRocks`, where the last life goes:
|
|
|
|
[source,lua]
|
|
----
|
|
if lives <= 0 then
|
|
state = DYING
|
|
deathChannel = channel
|
|
----
|
|
|
|
In `endGame`, two lines become one, and the nonsense combination is gone
|
|
because there is nothing left to combine:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
deathChannel = -1
|
|
state = OVER
|
|
musicStop(theme, 1500)
|
|
end
|
|
----
|
|
|
|
And in `startGame`, the two lines that cleared both flags become
|
|
`state = PLAYING`.
|
|
|
|
=== One Place That Decides
|
|
|
|
Every frame, `onOverlayUpdate` makes the two decisions that matter: what
|
|
moves, and what words go on top. What moves is one test:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == PLAYING then
|
|
updateShip()
|
|
updateShots()
|
|
updateRocks()
|
|
end
|
|
----
|
|
|
|
That replaces `if not over and not dying then`, and it says what it means. On
|
|
the title, while the last explosion plays, and after the game is over, nothing
|
|
moves. You get that by not calling the functions that move things. Stopping a
|
|
game is not a special feature; it is leaving things out.
|
|
|
|
The world is still drawn every frame, whatever the state, exactly as Dodge drew
|
|
it. Only the words change. Replace the `drawHud()` line with this:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == TITLE then
|
|
drawTitle()
|
|
else
|
|
drawHud()
|
|
end
|
|
----
|
|
|
|
`drawTitle` is new, and it goes after `drawStars`:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawTitle()
|
|
overlayPrint(25, 6, "R O C K S")
|
|
overlayPrint(25, 9, "BEST " .. best)
|
|
overlayPrint(22, 12, "PRESS 1 TO PLAY")
|
|
end
|
|
----
|
|
|
|
`best` is the best score so far, and it arrives in the next section; the game
|
|
will not run until it does. Before the first game the list of rocks is empty,
|
|
so what the title shows is the stars drifting past and the ship waiting at the
|
|
bottom, which is all a title screen needs.
|
|
|
|
One line has to go, or you will never see the title at all. Delete the
|
|
`startGame()` at the very bottom of the file. The game begins on its title now,
|
|
and *Starting Again* says why that line matters.
|
|
|
|
`drawHud` asks one question of its own. Change its `if over then` to:
|
|
|
|
[source,lua]
|
|
----
|
|
if state == OVER then
|
|
----
|
|
|
|
The input callback needs the same kind of thinking. Here it is whole:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
goLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
goRight = true
|
|
elseif what == SWITCH_BUTTON1 and state == PLAYING then
|
|
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
|
soundPlay(shootClip, 0, 35)
|
|
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
|
soundPlay(clickClip)
|
|
startGame()
|
|
end
|
|
end
|
|
----
|
|
|
|
Read it from the top. The arrow keys are recorded whatever the state, because
|
|
they only say which keys are down, and nothing moves the ship unless the state
|
|
is `PLAYING`. Fire means something only while playing. And `1` starts a game
|
|
from the title or from the game over screen, and not while the last explosion
|
|
is still sounding: the game is not over until it says so. The parentheses work
|
|
the way they do in arithmetic. The `or` inside them is worked out first, and
|
|
then the `and` joins it to the key.
|
|
|
|
=== Score and Lives
|
|
|
|
You have had both since lesson nine, and they hardly change. What is new is the
|
|
best score, which starts life as an ordinary variable. Put it with the others,
|
|
below `lives`:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = 0
|
|
----
|
|
|
|
Show it in the line across the top, between the other two. In `drawHud`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(1, 1, "SCORE " .. score)
|
|
overlayPrint(25, 1, "BEST " .. best)
|
|
overlayPrint(50, 1, "LIVES " .. lives)
|
|
----
|
|
|
|
And give `endGame` the job of noticing a record:
|
|
|
|
[source,lua]
|
|
----
|
|
if score > best then
|
|
best = score
|
|
end
|
|
----
|
|
|
|
Run it. The title says `BEST 0`. Press `1`, shoot something, lose, and the game
|
|
over screen shows your score as the best. Press `1` again and the best stays
|
|
while the score starts over. Now quit, run it again, and look at the title:
|
|
`BEST 0`. The record lived exactly as long as the program did. That is the
|
|
second half of this lesson.
|
|
|
|
While you are looking at lives, notice that lesson nine wrote the test as
|
|
`lives <= 0`, not `lives == 0`, and keep it that way. Today nothing can take
|
|
two lives at once, so the two can only ever agree. The day you add a rock that
|
|
costs two, `== 0` steps straight over zero and the player keeps playing on
|
|
minus one life, while `<= 0` keeps working. Ask for what you mean.
|
|
|
|
=== Starting Again
|
|
|
|
You have already deleted the `startGame()` from the bottom of the file. In
|
|
lesson ten the first game was started by that line and every later one by
|
|
pressing `1`, which is two ways in. Now there is one. Every game, the first
|
|
included, starts when somebody presses `1` and `startGame` runs, so the first
|
|
game and the fifth start exactly the same way.
|
|
|
|
`startGame` has to put back everything a game changes:
|
|
|
|
[source,lua]
|
|
----
|
|
function startGame()
|
|
shipX = (screenWidth - shipWidth) / 2
|
|
score = 0
|
|
lives = START_LIVES
|
|
state = PLAYING
|
|
deathChannel = -1
|
|
shots = {}
|
|
for i = 1, ROCK_COUNT do
|
|
rocks[i] = {}
|
|
newRock(rocks[i])
|
|
end
|
|
musicSetVolume(50)
|
|
musicPlay(theme, -1)
|
|
end
|
|
----
|
|
|
|
It is just as important to know what it leaves alone, and why. `best` outlives
|
|
every game; that is its whole job. The stars are scenery. And `goLeft` and
|
|
`goRight` are not the game's at all. They are the keyboard, and only the
|
|
keyboard should change them.
|
|
|
|
That last one is a bug you have just fixed without noticing. Lesson ten's
|
|
input callback started with `if over then`, and while the game was over it
|
|
threw every key away except `1`. Press the right arrow while GAME OVER is
|
|
showing, keep holding it, and press `1`. The new ship sits still: the press of
|
|
the right arrow was thrown away, the release has not happened yet, and so
|
|
`goRight` is `false` while the key is plainly down. The ship stays put until you let go and
|
|
press again. The new callback records the arrow keys in every state, so the
|
|
held key is still held when the game begins.
|
|
|
|
Bugs in starting again are the most common bugs in any game, and they all feel
|
|
the same to play: the second game is subtly wrong and the first one was fine.
|
|
When that happens, read `startGame` and ask two questions. What did the last
|
|
game change that this one does not put back? And what is this one putting back
|
|
that was never the game's to touch?
|
|
|
|
=== A Number That Outlives the Program
|
|
|
|
Everything your game has held so far -- `score`, `lives`, the list of rocks --
|
|
lives in memory, and memory is gone the instant the program stops.
|
|
|
|
Singe gives every game a save of its own: a set of names with values under
|
|
them, written to disk for you. It is another part of the game kit from lesson
|
|
eleven, so it is the same kind of thing as `collideRects`: plain calls, nothing
|
|
to load or free.
|
|
|
|
[source,lua]
|
|
----
|
|
saveSet("highScore", 4200)
|
|
----
|
|
|
|
`saveSet` takes a name -- the *key* -- and a value. The key is yours to choose
|
|
and never changes; the value is whatever you want to remember. Numbers,
|
|
strings, `true` and `false`, and whole tables of those all go in. Things that
|
|
only make sense while the program is running do not. A sprite handle is only a
|
|
number, so it saves, and tomorrow it is a number that means nothing. A function
|
|
cannot be written down at all, and trying to save one stops the game with an
|
|
error rather than letting you find out next week.
|
|
|
|
Reading it back is the other half. Change the line you added for `best`:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
It runs once, when the game starts. The second argument is the *default*: what
|
|
to answer when nothing has ever been saved under that key. More about that in a
|
|
moment, because it is the whole of this lesson's error.
|
|
|
|
And make `endGame` write the record down when it falls:
|
|
|
|
[source,lua]
|
|
----
|
|
function endGame()
|
|
deathChannel = -1
|
|
state = OVER
|
|
musicStop(theme, 1500)
|
|
if score > best then
|
|
best = score
|
|
saveSet("highScore", best)
|
|
saveFlush()
|
|
end
|
|
end
|
|
----
|
|
|
|
You do not have to ask Singe to write the file. It writes once at the end of
|
|
any frame in which something changed, however many keys you set, and again
|
|
when the game shuts down. `saveFlush` writes it immediately instead: a new high
|
|
score is worth a file write of its own, because an arcade cabinet gets
|
|
switched off at the wall and a frame is a long time.
|
|
|
|
Run it, set a score, quit, and run it again. The title remembers.
|
|
|
|
The reference's Save section lists the rest of the family. `saveDelete` forgets
|
|
one key, `saveClear` forgets everything -- that is the "reset high scores" in a
|
|
service menu -- and `saveGetAll` hands you a copy of the lot, which is useful
|
|
when you want to print the whole save while you are hunting something.
|
|
|
|
=== Where the File Actually Goes
|
|
|
|
It does not go beside your script, and this is not an accident.
|
|
|
|
A finished game may live on a disc, on a network share, or inside a single
|
|
packed file (lesson thirty), and none of those can be written to. A game that
|
|
saves next to its own script works perfectly on the machine you wrote it on
|
|
and fails on half the machines it is installed on. Singe 2 did it that way and
|
|
it was wrong.
|
|
|
|
So everything Singe writes for a game -- the save, screenshots, its log --
|
|
goes in one place, and `singeGetDataPath` will tell you where:
|
|
|
|
[source,lua]
|
|
----
|
|
debugPrint(singeGetDataPath())
|
|
----
|
|
|
|
Put that just below the `dofile` line for a moment and run it. It says
|
|
`data/rocks/`: the `data` folder that appeared in your work folder the first
|
|
time you ran anything, in lesson one, and inside it a folder named after your
|
|
game's folder. In there you will find `save.json`. Open it in your text editor: it is
|
|
plain text, and your high score is in there under the name you gave it.
|
|
|
|
That is the answer whenever you need a file of your own as well. Build the
|
|
name from `singeGetDataPath()` and it lands somewhere writable; write to a name
|
|
of your own invention and sooner or later it does not.
|
|
|
|
=== The First Time, There Is Nothing
|
|
|
|
The very first time anybody runs your game, no high score has ever been saved.
|
|
`saveGet` has nothing to hand back, so it hands back the default you gave it:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
Nothing saved, so `best` is `0`, the title screen says `BEST 0`, and the first
|
|
game anybody plays sets a record. That is exactly what you want, and it costs
|
|
one extra argument.
|
|
|
|
Leave the `0` out and `saveGet` has no default to fall back on, so it answers
|
|
`nil` -- the value Lua uses for "there is nothing here", which you met in lesson
|
|
three. `nil` is not zero. You cannot add to it, you cannot compare it with a
|
|
number, and you cannot glue it onto a string. It goes wrong on the first frame,
|
|
every time, on every machine where the game has not been played -- which
|
|
includes every machine your players are about to install it on, and not the one
|
|
you are testing on.
|
|
|
|
Give every `saveGet` a default. It is the cheapest habit in this book.
|
|
|
|
=== What Just Happened
|
|
|
|
[source,lua]
|
|
----
|
|
local TITLE = "title"
|
|
----
|
|
|
|
A constant: a name you set once and never change, in capitals so that you can
|
|
see at a glance that it is one, as in lesson seven. Lua does not enforce it --
|
|
nothing stops you assigning to `TITLE` -- so the capitals are a message to the
|
|
next person reading, who is you in a fortnight.
|
|
|
|
[source,lua]
|
|
----
|
|
if state == PLAYING then
|
|
----
|
|
|
|
The question that decides whether the world moves. When you add a fifth state
|
|
later -- a pause, or a bonus round -- the first thing to ask is whether the
|
|
world moves in it, and this is where the answer goes.
|
|
|
|
[source,lua]
|
|
----
|
|
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
|
----
|
|
|
|
Without the parentheses, Lua would join the `and` first and read this as "the
|
|
key is `1` and the state is the title, or the state is over". Then any key the
|
|
branches above did not catch would start a game from the game over screen,
|
|
fire included, and a player still hammering the fire button when the last
|
|
rock hits would be dropped into a new game the moment GAME OVER appeared. The
|
|
parentheses make the `or` happen first. When you mix `and` and `or`, add the
|
|
parentheses even if you think you know the order. The next person to read it
|
|
will not have to know.
|
|
|
|
[source,lua]
|
|
----
|
|
saveSet("highScore", best)
|
|
saveFlush()
|
|
----
|
|
|
|
`saveSet` puts the number in the save; `saveFlush` puts the save on the disk
|
|
now rather than at the end of the frame. Both are inside the `if`, so the file
|
|
is only written when the record is actually broken, which is a few times a
|
|
session rather than sixty times a second.
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore", 0)
|
|
----
|
|
|
|
Read once, at startup, into an ordinary variable. The rest of the game reads
|
|
`best` rather than calling `saveGet` again, because the save is a file and a
|
|
variable is a variable. Write to both when the record falls.
|
|
|
|
=== Try It
|
|
|
|
. *Go back to the title.* An arcade machine does not sit on GAME OVER forever.
|
|
Look up `timerAfter` in the reference's Timer section, another part of the
|
|
game kit, and use it in `endGame` to put the state back to `TITLE` five
|
|
seconds after the game ends. Then press `1` quickly after a game ends and make
|
|
sure the timer does not throw your new game back to the title three seconds
|
|
in.
|
|
. *Show the lives as ships.* Draw `shipSprite` once per life in the top right
|
|
corner with `spriteDraw` instead of printing the number. A `for` loop from
|
|
lesson four, and `shipWidth` tells you how far apart to put them.
|
|
. *Save more than a number.* Add `saveSet("gamesPlayed", saveGet("gamesPlayed",
|
|
0) + 1)` to `startGame`, and print it on the title screen. Quit, run again,
|
|
and watch it remember.
|
|
. *Find the file and break it.* Open `data/rocks/save.json` in your editor,
|
|
change the high score to something absurd, and run the game. Then delete the
|
|
file entirely and run it again. Both of those will happen to your players.
|
|
. *Reset it from inside the game.* Make a key you would never press by accident
|
|
call `saveClear()`, and check the title screen afterwards. Careful: `best` is
|
|
still in memory, and `saveClear` does not touch it. Work out what else has to
|
|
happen.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Take the default out, so the line reads:
|
|
|
|
[source,lua]
|
|
----
|
|
local best = saveGet("highScore")
|
|
----
|
|
|
|
If you have already set a high score, nothing happens at all, which is the
|
|
trap: the save has the key, so `saveGet` hands it back. Delete
|
|
`data/rocks/save.json` first, then run it. The title screen never appears, and
|
|
you get this:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best')
|
|
stack traceback:
|
|
rocks/rocks.singe:101: in function 'drawTitle'
|
|
rocks/rocks.singe:165: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Read it the way lesson eight taught you. The file and the line take you to
|
|
`overlayPrint(25, 9, "BEST " .. best)` in `drawTitle`, and the complaint says
|
|
something that was `nil` was glued onto a string. `..` joins two strings, and
|
|
`nil` is not one. If your lines are laid out differently, your numbers will
|
|
be too.
|
|
|
|
The line it names is not where the mistake is. Line 101 is where the game
|
|
finally tripped over it; the mistake was made near the top of the file, where
|
|
`best` was asked for and nothing was there. That gap between where a bad value
|
|
is made and where it is noticed is the hardest thing about `nil`, and the
|
|
reason a default costs one argument and saves an evening.
|
|
|
|
Put the `0` back.
|
|
|
|
=== What You Learned
|
|
|
|
* A game is in exactly one state at a time, and true-or-false flags that try
|
|
to say which one breed combinations that mean nothing.
|
|
* Keep the state in one variable, with its possible values named once at the
|
|
top of the file.
|
|
* `onOverlayUpdate` decides what moves and what is said, by asking the state.
|
|
* Stopping a game is a matter of not calling the things that move it.
|
|
* Start every game, the first one included, through one function, and make
|
|
that function put back everything a game changes -- and nothing else.
|
|
* Keys belong to the keyboard. Record them in every state.
|
|
* `saveSet` stores a value under a key, and `saveGet` reads it back on a later
|
|
run.
|
|
* `saveFlush` writes the file now, for the things you would hate to lose.
|
|
* The save goes in the game's own data folder, which `singeGetDataPath`
|
|
names. Never write beside your script.
|
|
* `saveGet` with no default answers `nil` when nothing was saved, and `nil`
|
|
breaks the first frame on a machine that has never run your game.
|
|
* When you mix `and` and `or`, use parentheses.
|
|
|
|
=== Next Time
|
|
|
|
Your game is a game now, and it is a folder that only you know how to start.
|
|
Lesson thirteen gives it a `games.dat`, so the engine's own menu lists it with
|
|
a title and a picture, and a player who has never heard of you can start it
|
|
without typing anything.
|