505 lines
18 KiB
Text
505 lines
18 KiB
Text
== Lesson 3: Making Decisions
|
|
|
|
image::learn/03-decisions.png[The finished lesson, 480]
|
|
|
|
Everything you have written so far does the same thing every frame. The word in
|
|
lesson two moved whether you wanted it to or not, and the only reason it ever
|
|
turned round was four blocks of code you were told to take on trust.
|
|
|
|
This lesson is those four blocks, properly. By the end of it your program will
|
|
ask questions, act on the answers, and -- because the most interesting question
|
|
a game can ask is "what is the player doing?" -- you will be steering a box
|
|
around the window with the arrow keys. Keep that box. It is the player of the
|
|
game you build in lesson seven.
|
|
|
|
=== Start a New Folder
|
|
|
|
A folder called `decisions`, a file in it called `decisions.singe`, and
|
|
`Singe -R decisions` running in a terminal. Same as last time.
|
|
|
|
=== What an `if` Really Says
|
|
|
|
Here is the block from lesson two on its own.
|
|
|
|
[source,lua]
|
|
----
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
end
|
|
----
|
|
|
|
Four parts. The word `if`. A question. The word `then`. And a body of lines
|
|
that run only when the answer to the question is yes, closed off by `end`.
|
|
|
|
The question is `wordX > lastColumn`, and the `>` is doing the real work. It
|
|
compares the two numbers either side of it and produces an answer that is
|
|
either *true* or *false*. Those two words are not text and are not numbers;
|
|
they are values in their own right, called *booleans*, and a boolean is the
|
|
only thing an `if` is interested in.
|
|
|
|
The indentation is the same idea as in lesson one: the lines inside are indented
|
|
so you can see at a glance what belongs to the `if`, and the `end` lines up with
|
|
the `if` that opened it. Lua would run it without the spaces. You would not be
|
|
able to read it.
|
|
|
|
There are six of these comparisons and you will use all of them.
|
|
|
|
[cols="1,4"]
|
|
|===
|
|
| `a < b` | a is less than b
|
|
| `a > b` | a is greater than b
|
|
| `a <= b` | a is less than or equal to b
|
|
| `a >= b` | a is greater than or equal to b
|
|
| `a == b` | a is the same as b
|
|
| `a ~= b` | a is not the same as b
|
|
|===
|
|
|
|
The last two are the ones to be careful with, for different reasons.
|
|
|
|
`~=` means "is not equal to". Most languages write that `!=`, and if you have
|
|
seen a program before, your fingers will type `!=` and Lua will refuse it. The
|
|
squiggle is on your keyboard next to the `1` or under the `#`, depending on
|
|
where you live.
|
|
|
|
`==` is two equals signs, and it is not a typo for one. This is the single most
|
|
common mistake in programming, and it is worth stopping on for a moment.
|
|
|
|
In lesson two you learned that one equals sign means *put the right-hand side
|
|
into the name on the left*. That is a thing you do. It changes something.
|
|
|
|
Two equals signs mean *are these the same?* That is a question you ask. It
|
|
changes nothing, and it hands back true or false.
|
|
|
|
[source,lua]
|
|
----
|
|
lives = 3
|
|
if lives == 3 then
|
|
----
|
|
|
|
The first line sets `lives` to three. The second asks whether it is three.
|
|
Different jobs, different symbols, one character apart. Lua will catch you if
|
|
you get it the wrong way round, and you will meet the error it uses at the end
|
|
of this lesson.
|
|
|
|
=== When the Answer Is No
|
|
|
|
An `if` on its own does nothing at all when the answer is false. Very often you
|
|
want the other thing to happen instead, and that is `else`.
|
|
|
|
[source,lua]
|
|
----
|
|
if lives > 0 then
|
|
message = "Ready"
|
|
else
|
|
message = "Game Over"
|
|
end
|
|
----
|
|
|
|
Exactly one of those two bodies runs, never both, never neither, so `message`
|
|
always ends up holding one of the two strings and never nothing.
|
|
|
|
When there are more than two possibilities, `elseif` chains them.
|
|
|
|
[source,lua]
|
|
----
|
|
if score >= 10000 then
|
|
rank = "ace"
|
|
elseif score >= 5000 then
|
|
rank = "pilot"
|
|
elseif score >= 1000 then
|
|
rank = "rookie"
|
|
else
|
|
rank = "passenger"
|
|
end
|
|
----
|
|
|
|
Lua tries the questions from the top and stops at the first one that is true. A
|
|
score of 6000 is not greater than 10000, so it tries the next, which is true,
|
|
so `rank` becomes `pilot` and the other two questions are never asked. That
|
|
last point matters: the `elseif` for 1000 is also true for a score of 6000, and
|
|
it is never reached, which is why the order is from biggest to smallest.
|
|
|
|
The whole chain needs exactly one `end`, no matter how many `elseif` parts are
|
|
in it. It is one decision, not four.
|
|
|
|
=== True, False, and Nothing at All
|
|
|
|
You do not have to compare two things to get a boolean. You can keep one.
|
|
|
|
[source,lua]
|
|
----
|
|
paused = false
|
|
----
|
|
|
|
That is a variable like any other, and the value in it is the boolean `false`
|
|
itself, written without quotes because it is not the word "false", it is the
|
|
idea. The same goes for `true`. A variable holding one of them can go straight
|
|
into an `if` with no comparison at all:
|
|
|
|
[source,lua]
|
|
----
|
|
if paused then
|
|
----
|
|
|
|
Read it as "if paused". That is better English than `if paused == true`, which
|
|
does the same thing the long way round, and experienced programmers will look
|
|
at you oddly for writing it.
|
|
|
|
Three words join and flip these answers.
|
|
|
|
[source,lua]
|
|
----
|
|
if alive and not shielded then
|
|
if coins > 0 or freePlay then
|
|
----
|
|
|
|
`and` is true when both sides are. `or` is true when either side is, or both.
|
|
`not` turns true into false and false into true. They read like English and,
|
|
unusually for programming, they mean what English means.
|
|
|
|
There is one more value to know about, and it is the one that produces the most
|
|
confusing errors of your first year.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "lives " .. tostring(lives))
|
|
----
|
|
|
|
If you have never put anything into `lives`, that prints `lives nil`. Lua does
|
|
not complain about a name it has never seen; it hands you `nil`, which is its
|
|
word for *nothing here*. It is not zero and it is not an empty string. Zero is
|
|
a number you meant to store. `nil` is the absence of a value.
|
|
|
|
You have met `nil` twice already without being told. `frames = frames + 1`
|
|
without the `frames = 0` line above it fails because you asked Lua to add one
|
|
to nothing. And a callback you spell wrong is `nil` too, which is why a
|
|
misspelled `onOverlayUpdate` gives you a black window and no error at all:
|
|
Singe looks for the name, finds nothing there, and quietly gets on with its
|
|
day.
|
|
|
|
`tostring` is in that line because `..` will not join a boolean or a `nil` to
|
|
anything. Numbers it converts for you; these it will not. That was the promise
|
|
at the end of lesson two, and now you have a use for it.
|
|
|
|
Finally, the rule that ties the whole section together, because `if` will
|
|
accept anything you give it, not only true and false:
|
|
|
|
[quote]
|
|
Everything in Lua is true except `false` and `nil`.
|
|
|
|
Zero is true. An empty string is true. Only those two are not, and once you know
|
|
it, `if movingLeft then` reads perfectly whether `movingLeft` holds `true`,
|
|
`false`, or has never been set at all.
|
|
|
|
=== Reading the Controls
|
|
|
|
Time to ask the player something. That takes a new callback.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_RIGHT then
|
|
boxX = boxX + 10
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputPressed` is the second callback you have written, and it works exactly
|
|
the way lesson one described: you write it, Singe calls it. `onOverlayUpdate`
|
|
is called on a timer. This one is called when something happens -- when the
|
|
player presses anything the engine recognizes.
|
|
|
|
The difference from `onOverlayUpdate` is the word inside the parentheses.
|
|
Singe has to tell you *which* control was pressed, so it hands the function a
|
|
value, and `what` is the name you have chosen to receive it under. Lesson one
|
|
called the things you pass into a function arguments; from the inside, the name
|
|
that catches one is a *parameter*. The name is yours: call it `what`, call it
|
|
`button`, call it `k`. Singe fills it in either way.
|
|
|
|
`SWITCH_RIGHT` is a name the engine has already given to a value, like
|
|
`OVERLAY_UPDATED` in lesson one. You do not have to load anything to use it;
|
|
Singe defines all of them before your script runs. There is one for everything
|
|
a cabinet has:
|
|
`SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, `SWITCH_BUTTON1`
|
|
through `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1`, and more. The manual
|
|
lists them all. They are deliberately not called "the right arrow key", because
|
|
the same `SWITCH_RIGHT` arrives whether the player pushed an arrow key, a
|
|
joystick, or a control pad's thumbstick. You write the game once and it works
|
|
on all three.
|
|
|
|
Add that callback, give `boxX` a starting value outside the functions, and draw
|
|
something at `boxX`. Press the right arrow. The box jumps ten pixels each time
|
|
you press it.
|
|
|
|
=== It Happened, and It Is Happening
|
|
|
|
Hold the arrow key down. Nothing more happens.
|
|
|
|
`onInputPressed` is called once, on the way down, and not again until you let
|
|
go and press it again. There is no key repeat. That is exactly right for some
|
|
things -- firing a shot, inserting a coin, choosing a menu item -- and exactly
|
|
wrong for walking, which needs to keep happening for as long as the key is
|
|
held.
|
|
|
|
The two are genuinely different questions. *It happened* is an event, and you
|
|
hear about it once. *It is happening* is a state, and it lasts. Singe tells you
|
|
about events, and the way you turn events into state is to remember them
|
|
yourself.
|
|
|
|
Two callbacks, a variable between them.
|
|
|
|
[source,lua]
|
|
----
|
|
movingRight = false
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_RIGHT then
|
|
movingRight = true
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_RIGHT then
|
|
movingRight = false
|
|
end
|
|
end
|
|
----
|
|
|
|
`onInputReleased` is the third callback, and it is the mirror of the second:
|
|
Singe calls it on the way back up, with the same `SWITCH_` value. Between them
|
|
they keep `movingRight` telling the truth about what the player's finger is
|
|
doing right now.
|
|
|
|
Nothing has moved yet, and that is the point. The pressing and the moving are
|
|
now two separate jobs. The moving goes where all the per-frame work goes.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
if movingRight then
|
|
boxX = boxX + BOX_SPEED
|
|
end
|
|
...
|
|
----
|
|
|
|
The box now slides smoothly for as long as the key is down, at a speed you
|
|
control, and the arithmetic is back in the one function that runs sixty times a
|
|
second where it belongs. This shape -- events set flags, the frame acts on
|
|
flags -- is how nearly every game handles its controls, and you will use it
|
|
again in every lesson that has a player in it.
|
|
|
|
=== Drawing a Box
|
|
|
|
`overlayBox` takes two corners: the left and top of the rectangle, then the
|
|
right and bottom.
|
|
|
|
[source,lua]
|
|
----
|
|
colorForeground(255, 220, 0)
|
|
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
|
|
----
|
|
|
|
It draws an *outline*, four lines one pixel thick, and not a filled rectangle.
|
|
There is no filled rectangle in the overlay drawing calls at all. When you want
|
|
a solid block later there are ways to get one, and lesson nine has the usual
|
|
one.
|
|
|
|
Its numbers are pixels, not character cells. This catches people out, because
|
|
`overlayPrint` in lesson two counted in cells of six pixels by thirteen and
|
|
everything else in Singe counts in pixels. `overlayPrint` is the odd one; the
|
|
rest of the drawing calls -- `overlayBox`, `overlayLine`, `overlayCircle`,
|
|
`overlayEllipse`, and `overlayPlot` -- all work in the same pixels that
|
|
`overlayGetWidth` reports.
|
|
|
|
`colorForeground` sets the colour, as three numbers from 0 to 255: how much
|
|
red, how much green, how much blue. All three at 255 is white, all at 0 is
|
|
black, and `(255, 220, 0)` is a warm yellow. Notice that it is not an argument
|
|
to `overlayBox`. It is a setting: you change it, and everything you draw after
|
|
that comes out in the new colour until you change it again. `overlayPrint` is
|
|
the exception once more -- the console font arrives with its own colours baked
|
|
in and ignores you.
|
|
|
|
=== The Whole Thing
|
|
|
|
[source,lua]
|
|
----
|
|
BOX_SIZE = 20
|
|
BOX_SPEED = 2
|
|
|
|
width = overlayGetWidth()
|
|
height = overlayGetHeight()
|
|
maxX = width - BOX_SIZE - 1
|
|
maxY = height - BOX_SIZE - 1
|
|
|
|
boxX = width // 2
|
|
boxY = height // 2
|
|
|
|
movingLeft = false
|
|
movingRight = false
|
|
movingUp = false
|
|
movingDown = false
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = true
|
|
elseif what == SWITCH_UP then
|
|
movingUp = true
|
|
elseif what == SWITCH_DOWN then
|
|
movingDown = true
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = false
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = false
|
|
elseif what == SWITCH_UP then
|
|
movingUp = false
|
|
elseif what == SWITCH_DOWN then
|
|
movingDown = false
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
if movingLeft and not movingRight then
|
|
boxX = boxX - BOX_SPEED
|
|
elseif movingRight and not movingLeft then
|
|
boxX = boxX + BOX_SPEED
|
|
end
|
|
|
|
if movingUp and not movingDown then
|
|
boxY = boxY - BOX_SPEED
|
|
elseif movingDown and not movingUp then
|
|
boxY = boxY + BOX_SPEED
|
|
end
|
|
|
|
if boxX < 0 then
|
|
boxX = 0
|
|
elseif boxX > maxX then
|
|
boxX = maxX
|
|
end
|
|
|
|
if boxY < 0 then
|
|
boxY = 0
|
|
elseif boxY > maxY then
|
|
boxY = maxY
|
|
end
|
|
|
|
overlayClear()
|
|
|
|
if movingLeft or movingRight or movingUp or movingDown then
|
|
colorForeground(255, 220, 0)
|
|
else
|
|
colorForeground(80, 160, 255)
|
|
end
|
|
|
|
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
|
|
overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Four things in there are worth a second look.
|
|
|
|
`movingLeft and not movingRight` is there because a player can hold both arrows
|
|
at once. Without the `not`, the two would fight and the box would jitter or
|
|
drift; with it, holding both means neither wins and the box stands still, which
|
|
is what a player expects.
|
|
|
|
`maxX` is worked out once at the top, using `overlayGetWidth` the way lesson two
|
|
did, and the `- 1` is because the surface's last pixel is one less than its
|
|
width. Take the `- 1` out and the box's right-hand edge falls off the surface
|
|
and vanishes, because Singe throws away anything you draw outside the overlay
|
|
without complaining.
|
|
|
|
The clamping blocks use `elseif` rather than two separate `if` blocks. The box
|
|
cannot be off the left edge and off the right edge at the same time, so asking
|
|
the second question when the first was true is wasted work.
|
|
|
|
And the colour decision is an `if` whose only job is to pick a value. Yellow
|
|
while the player is doing something, blue while they are not, which is a free
|
|
way of seeing that your input handling actually works.
|
|
|
|
=== Try It
|
|
|
|
. *Change the speed.* `BOX_SPEED = 1` and `BOX_SPEED = 8`. At eight, watch what
|
|
the clamping does when you shove the box into a corner.
|
|
. *Show your flags.* Add a line to `onOverlayUpdate`:
|
|
`overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Hold and release the
|
|
left arrow and watch it flip. Then take the `tostring` off and read the error
|
|
Lua gives you.
|
|
. *Make it faster when a button is down.* Add a `running` flag driven by
|
|
`SWITCH_BUTTON1` in the two input callbacks, and use an `if` in
|
|
`onOverlayUpdate` to move by twice `BOX_SPEED` while it is true.
|
|
. *Mark the middle.* Draw a second, small box in the centre of the window that
|
|
changes colour when the player's box overlaps it. You will need `and` to join
|
|
four comparisons, and it is harder than it sounds. Getting it slightly wrong
|
|
is normal; lesson eleven does it properly.
|
|
. *Take the releases out.* Delete the whole `onInputReleased` function and run
|
|
it. The box takes one step and then never stops. Work out which line is now
|
|
never reached.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Find the clamping block and take one of the equals signs out of a comparison,
|
|
so it reads:
|
|
|
|
[source,lua]
|
|
----
|
|
if boxX = 0 then
|
|
----
|
|
|
|
Save it. The window sits there empty and the terminal says:
|
|
|
|
----
|
|
Error running script: decisions.singe:57: 'then' expected near '='
|
|
----
|
|
|
|
This is the mistake the whole `==` section was warning about, and the message
|
|
is a good example of an error that is completely accurate and completely
|
|
unhelpful if you take it literally. Lua is not asking you to add the word
|
|
`then`. Lua got as far as `if boxX`, expected a comparison or a `then` next,
|
|
and found `=`, which cannot be either.
|
|
|
|
Whenever you see `'then' expected` after an `if`, look for a single `=` that
|
|
should be a double one. It is nearly always that.
|
|
|
|
Note where this one appeared: Singe never opened the window, the same as lesson
|
|
one's unfinished string, because Lua could not read the file. The frame counter
|
|
error in lesson two happened while running. Errors in the shape of your code
|
|
stop you before the game starts. Errors in what your code does wait until it
|
|
runs.
|
|
|
|
=== What You Learned
|
|
|
|
* `if question then ... end` runs a body only when the answer is true.
|
|
* `else` gives the other case, and `elseif` chains more; the first true
|
|
question wins and the rest are never asked.
|
|
* The comparisons are `<`, `>`, `<=`, `>=`, `==`, and `~=`.
|
|
* `=` puts a value into a name. `==` asks whether two things are the same.
|
|
* `true` and `false` are values, and you can keep them in variables.
|
|
* `and`, `or`, and `not` join and flip those answers.
|
|
* `nil` is what you get from a name you never set, and it is not zero.
|
|
* Everything is true except `false` and `nil`.
|
|
* `onInputPressed` and `onInputReleased` are callbacks Singe calls with a
|
|
`SWITCH_` value when the player presses or lets go of a control.
|
|
* A press is an event and happens once; hold the state in a variable and act on
|
|
it every frame.
|
|
* `overlayBox` draws an outline in pixels, and `colorForeground` is a setting,
|
|
not an argument.
|
|
|
|
=== Next Time
|
|
|
|
One box is easy. A hundred stars are not, at least not the way you have written
|
|
things so far, because typing a hundred `overlayPlot` lines is nobody's idea of
|
|
a good evening. Lesson four is the loop: telling the computer to do the same
|
|
thing many times, with something different each time round, which is what makes
|
|
a screen full of anything possible.
|