404 lines
14 KiB
Text
404 lines
14 KiB
Text
== Lesson 2: Numbers That Change
|
|
|
|
image::learn/02-numbers.png[The finished lesson, 480]
|
|
|
|
In lesson one you put a word on the screen and it sat there. By the end of this
|
|
lesson the same word will be sliding around the window, turning round when it
|
|
reaches an edge, and it will be doing it because of four numbers that the
|
|
program changes sixty times a second.
|
|
|
|
Along the way you will meet the single idea that trips up more beginners than
|
|
any other: where you put something decides how long it lives.
|
|
|
|
=== Start a New Folder
|
|
|
|
Make a folder called `numbers` next to your `hello` folder, and inside it a
|
|
file called `numbers.singe`. Type the lesson one program into it, or copy
|
|
`hello.singe` over and rename it -- either is fine, and typing it again is not
|
|
wasted.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Run it with `Singe -R numbers` and leave it running. The `-R` from the
|
|
end of lesson one means you will not have to start it again for the rest of
|
|
this lesson; save the file and the window catches up on its own.
|
|
|
|
=== Count the Frames
|
|
|
|
Lesson one said that Singe calls `onOverlayUpdate` about sixty times a second.
|
|
That is easy to say and hard to believe, so count them.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
frames = 0
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Save it. In the top left corner you get `frame 1`, and it stays at `frame 1`
|
|
forever.
|
|
|
|
That is disappointing, and it is also the most useful thing in this lesson, so
|
|
do not fix it yet. Look at what you wrote and think about the order of the
|
|
lines, which lesson one said was the most important thing about a program.
|
|
|
|
Singe calls `onOverlayUpdate`. The first line sets `frames` to zero. The
|
|
second adds one to it, making one. You print it, you return, and the function
|
|
is over. Then Singe calls it again, and the first thing that happens is that
|
|
`frames` is set back to zero.
|
|
|
|
You are not counting frames. You are counting to one, sixty times a second.
|
|
|
|
=== Move the Line Out
|
|
|
|
Take the `frames = 0` line out of the function and put it above, on its own,
|
|
before the word `function`.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Save it. The number climbs, fast, and does not stop.
|
|
|
|
Nothing else changed. The only difference is *where* the line is, and that is
|
|
the whole of the idea. Lines inside a function happen every single time the
|
|
function is called. Lines outside every function happen once, when Singe reads
|
|
your file, before the game starts.
|
|
|
|
`frames = 0` needed to happen once. It was happening sixty times a second, and
|
|
it was wiping out the work of the line under it.
|
|
|
|
Every moving thing you ever write has this shape. Set the number up outside,
|
|
change it inside.
|
|
|
|
=== What Just Happened
|
|
|
|
Three new pieces of code are in there. Here they are on their own.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
----
|
|
|
|
This makes up a name, `frames`, and puts the number zero behind it. A name
|
|
with a value behind it is a *variable*, and it is called that because the value
|
|
is allowed to vary -- to change while the program runs.
|
|
|
|
The `=` is not the equals sign from school. It does not say that `frames` is
|
|
equal to zero; it says *put zero into `frames`*, and it always works in that
|
|
direction, right into left. Programmers call it *assignment*, and it is worth
|
|
saying the line out loud that way as you write it: "frames gets zero".
|
|
|
|
You choose the name. It can be almost anything that starts with a letter and
|
|
has no spaces in it, and it should say what the thing is. `frames` is a good
|
|
name. `f` is a bad one, and you will not thank yourself for it in a week.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = frames + 1
|
|
----
|
|
|
|
This is the line that makes people stop and stare, because as a statement about
|
|
arithmetic it is nonsense: nothing is equal to itself plus one.
|
|
|
|
It is not a statement about arithmetic. Remember that `=` puts the right-hand
|
|
side into the left-hand name, and read it in that order, right first. Work out
|
|
`frames + 1` using whatever is in `frames` right now. Then put the answer back
|
|
into `frames`. If `frames` held six, the right-hand side works out to seven,
|
|
and seven goes into `frames`.
|
|
|
|
Adding one to a variable like this is something you will write thousands of
|
|
times.
|
|
|
|
The `+` is what you expect, and so are the other three: `-` subtracts, `*`
|
|
multiplies, and `/` divides. There is no `x` key for multiply, which is why the
|
|
star does that job.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
----
|
|
|
|
`overlayPrint` wants text, and `frames` is a number, not text. The two dots
|
|
join the two together and hand the result over as one string: the four
|
|
characters `frame`, a space, and then however many digits the number needs.
|
|
|
|
Those two dots are the joining operator, and it is worth knowing its real name
|
|
because the error messages use it: joining strings is called *concatenation*.
|
|
|
|
Note the space inside the quotes, before the closing one. Without it you get
|
|
`frame6`. Lua joins exactly what you give it and adds nothing of its own.
|
|
|
|
Lua will turn a number into text for you when you join it to a string this
|
|
way, which is why you did not have to do anything special. It will not do that
|
|
for everything. If you ever try to join something that is neither text nor a
|
|
number, Lua stops with an error, and the way round it is `tostring`, which
|
|
turns anything at all into text:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. tostring(frames))
|
|
----
|
|
|
|
That line does exactly the same as the one before it. You do not need
|
|
`tostring` for numbers. You will need it in lesson three.
|
|
|
|
=== Make It Move
|
|
|
|
You now have everything you need to move the word. The word is drawn wherever
|
|
you say, you have a way to keep a number between frames, and you have a way to
|
|
change it.
|
|
|
|
Give the column its own variable outside the function, and add to it inside.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
wordX = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
wordX = wordX + 1
|
|
overlayClear()
|
|
overlayPrint(wordX, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Save it. The word shoots off the right-hand side and never comes back.
|
|
|
|
It moved one column every frame, sixty times a second, so it crossed the whole
|
|
window in about a second. Slow it down by adding less than a whole column:
|
|
|
|
[source,lua]
|
|
----
|
|
wordX = wordX + 0.25
|
|
----
|
|
|
|
Now it takes four frames to move one column, and the word creeps. `wordX` is
|
|
mostly a number like `12.75`, which is not a column at all, and Singe deals
|
|
with that by throwing away everything after the dot when it draws: `12.75`
|
|
draws in column twelve. Keeping the fraction in the variable is exactly what
|
|
lets you move slower than one column a frame. The number is finer than the
|
|
screen is.
|
|
|
|
=== Where Is the Edge?
|
|
|
|
The word still leaves. To turn it round you have to know where the right-hand
|
|
edge is, and you could find that out by counting: a game with no video gets a
|
|
world 720 by 480 by default, the overlay you draw on is half that, 360 by 240,
|
|
and the built-in console font's letters are six pixels wide, so there are sixty
|
|
columns.
|
|
|
|
Do not do that. Ask the engine.
|
|
|
|
[source,lua]
|
|
----
|
|
lastColumn = overlayGetWidth() // CELL_WIDTH - string.len(word)
|
|
----
|
|
|
|
`overlayGetWidth` and `overlayGetHeight` are two more functions Singe provides,
|
|
like `overlayPrint`. They take nothing and hand back a number: how wide and how
|
|
tall the drawing surface is. You saw `return` hand a value back in lesson one,
|
|
and this is the other end of it -- a function you call can give you something,
|
|
and here you are doing arithmetic with what it gave you.
|
|
|
|
The numbers they give you are in pixels, and `overlayPrint` counts in character
|
|
cells, so you have to divide. `overlayGetFontWidth` tells you how many pixels
|
|
wide one cell is, and `overlayGetFontHeight` how tall, so you never have to
|
|
know the answer yourself. The two slashes mean *divide and throw the fraction
|
|
away*, because a third of a character cell is not a place you can print. The
|
|
ordinary single slash would give you a fraction, and a fraction of a cell is
|
|
not a place either.
|
|
|
|
`string.len` counts the characters in a string, and it is there so that the
|
|
word turns round when its *last* letter reaches the edge rather than its first.
|
|
Try it without and you will see six letters slide off into nothing.
|
|
|
|
Those two capitalized names are a habit rather than a rule. Lua does not care,
|
|
but a name in capitals is how programmers say "this is a number I set once at
|
|
the top and never touch again", and it beats having a bare `6` halfway down a
|
|
file that nobody can explain a month later.
|
|
|
|
=== Turn It Round
|
|
|
|
Moving the other way is no work at all: instead of adding `0.25` every frame,
|
|
add `-0.25`. So keep the amount in a variable of its own and flip its sign when
|
|
the word arrives at an edge.
|
|
|
|
[source,lua]
|
|
----
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
speedX = -speedX
|
|
end
|
|
----
|
|
|
|
That is a decision, and decisions are the whole of lesson three, so take it on
|
|
trust for one lesson. Read it out: *if* `wordX` has got past `lastColumn`,
|
|
*then* do the two indented lines; otherwise skip them. The `end` closes it, the
|
|
same way `end` closes a function.
|
|
|
|
The first line pins the word exactly on the edge, so that a fast word cannot
|
|
sail past it. The second is the flip. `-speedX` is minus whatever `speedX`
|
|
holds, so if it held `0.25` it now holds `-0.25`, and adding `-0.25` every
|
|
frame walks the word back the way it came. When it reaches the left edge, the
|
|
matching `if` flips it again, and it flips back to `0.25`.
|
|
|
|
Two more of those for the top and bottom edges and the word bounces around the
|
|
whole window.
|
|
|
|
=== The Whole Thing
|
|
|
|
[source,lua]
|
|
----
|
|
CELL_WIDTH = overlayGetFontWidth()
|
|
CELL_HEIGHT = overlayGetFontHeight()
|
|
TOP_ROW = 2
|
|
|
|
word = "Hello!"
|
|
wordX = 0
|
|
wordY = TOP_ROW
|
|
speedX = 0.25
|
|
speedY = 0.125
|
|
frames = 0
|
|
|
|
lastColumn = overlayGetWidth() // CELL_WIDTH - string.len(word)
|
|
lastRow = overlayGetHeight() // CELL_HEIGHT - 1
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
wordX = wordX + speedX
|
|
wordY = wordY + speedY
|
|
|
|
if wordX < 0 then
|
|
wordX = 0
|
|
speedX = -speedX
|
|
end
|
|
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
speedX = -speedX
|
|
end
|
|
|
|
if wordY < TOP_ROW then
|
|
wordY = TOP_ROW
|
|
speedY = -speedY
|
|
end
|
|
|
|
if wordY > lastRow then
|
|
wordY = lastRow
|
|
speedY = -speedY
|
|
end
|
|
|
|
overlayClear()
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
overlayPrint(wordX, wordY, word)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
`TOP_ROW` keeps the word out of the top two lines so that it never scribbles
|
|
over the frame counter. `lastRow` has a `- 1` in it rather than a
|
|
`string.len`, because a word is many characters wide but only ever one line
|
|
tall.
|
|
|
|
Notice that the whole top half of the file runs once and the whole bottom half
|
|
runs sixty times a second, and that you can tell which is which by looking at
|
|
the indentation. That is what the spaces were for.
|
|
|
|
=== Try It
|
|
|
|
. *Change the speeds.* Try `speedX = 1` and `speedY = 1`. Then try `0.05` for
|
|
both. Then make one much bigger than the other.
|
|
. *Change the word.* Put a much longer word in the quotes -- long enough to
|
|
fill a third of the window -- and watch the right-hand bounce still happen in
|
|
the right place. That is `string.len` doing its job.
|
|
. *Take the clear out.* Delete the `overlayClear()` line. Lesson one promised
|
|
this would matter one day, and this is the day: the word smears a trail
|
|
across the window and never rubs any of it out, because drawing has always
|
|
drawn on top of what was there. Put it back.
|
|
. *Stop it going sideways.* Set `speedX = 0` and run it. Work out why the word
|
|
now never leaves column zero, and why the `if` for the left edge never fires.
|
|
. *Break the frame counter on purpose.* Move the `frames = 0` line back inside
|
|
the function, above `frames = frames + 1`. Watch the counter stick at one
|
|
while the word keeps moving perfectly. Then work out why the word is
|
|
unaffected.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Change the two dots on the frame counter line to a plus sign, so that it reads:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " + frames)
|
|
----
|
|
|
|
Save it. The window closes, Singe stops, and the terminal says:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': numbers.singe:42: attempt to add a 'string' with a 'number'
|
|
----
|
|
|
|
followed by several lines starting with `stack traceback:`, which you can
|
|
ignore for now.
|
|
|
|
Read it the way lesson one taught you: the file, the line, then the complaint.
|
|
Line 42 is the line you changed. The complaint says Lua was asked to *add* a
|
|
string to a number, and adding is for numbers. `+` and `..` look similar on the
|
|
page and do entirely different jobs: `+` is arithmetic, `..` is glue.
|
|
|
|
This one is worth meeting because of where it appeared. Lesson one's error
|
|
stopped Singe before the window ever opened, because Lua could not read the
|
|
file at all. This one is a perfectly readable line that only goes wrong when it
|
|
runs, so the window opened, the game ran, and it died on the first frame. Both
|
|
kinds tell you the file and the line. Start there either way.
|
|
|
|
=== What You Learned
|
|
|
|
* A variable is a name with a value behind it, and the value can change.
|
|
* `=` means "put the right-hand side into the name on the left", not "is equal
|
|
to".
|
|
* `frames = frames + 1` works out the right-hand side first, then stores it.
|
|
* `+`, `-`, `*`, and `/` do the arithmetic you expect; `//` divides and throws
|
|
the fraction away.
|
|
* `..` joins text together, and turns a number into text on the way.
|
|
* A line inside `onOverlayUpdate` runs every frame. A line outside every
|
|
function runs once, before the game starts.
|
|
* Anything that has to remember something between frames has to live outside.
|
|
* `overlayGetWidth` and `overlayGetHeight` tell you the size of the drawing
|
|
surface, so you never have to guess it.
|
|
* `overlayClear` matters the moment anything moves.
|
|
|
|
=== Next Time
|
|
|
|
The word moves, but it moves on its own and nothing you do makes any
|
|
difference to it. In lesson three you take the four `if` blocks you used on
|
|
trust, learn what they really say, and point them at the arrow keys instead of
|
|
the edges of the screen. The box you steer around the window at the end of it
|
|
is the player of the game you build in lesson seven.
|