395 lines
15 KiB
Text
395 lines
15 KiB
Text
== Lesson 4: Doing It Again
|
|
|
|
image::learn/04-repeating.png[The finished lesson, 480]
|
|
|
|
You have drawn one word and one box. A game needs a hundred of things: stars,
|
|
bricks, bullets, the rows of a menu. Typing a hundred lines is not the answer,
|
|
and if it were, you would have to type another hundred the moment you wanted a
|
|
hundred and one.
|
|
|
|
This lesson is the answer. By the end of it a field of stars will be drifting
|
|
down your window, every one of them drawn by the same three lines of code.
|
|
|
|
=== Start a New Folder
|
|
|
|
A folder called `repeating`, a file in it called `repeating.singe`, and
|
|
`Singe -R repeating` running in a terminal.
|
|
|
|
=== Forty Dots
|
|
|
|
Type this in and run it.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
colorForeground(255, 255, 255)
|
|
|
|
for star = 1, 40 do
|
|
overlayPlot(star * 8, 100)
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
A row of forty white dots across the middle of the window.
|
|
|
|
`overlayPlot` is the simplest drawing call there is: it sets one pixel to the
|
|
current foreground colour, and it is exactly what a star wants. Everything
|
|
interesting here is the three lines around it.
|
|
|
|
[source,lua]
|
|
----
|
|
for star = 1, 40 do
|
|
----
|
|
|
|
That says: do what follows forty times, and each time round, `star` holds a
|
|
different number -- one the first time, two the second, forty the last. Both
|
|
ends are included, so `1, 40` really is forty times round and not thirty-nine.
|
|
This shape is a *loop*, and this particular one is a *numeric for loop*.
|
|
|
|
`star` is a name you invent, the same way you invented `frames` in lesson two,
|
|
and the loop fills it in for you. You do not set it, and you should not change
|
|
it inside the loop. It exists only between the `for` and its `end`; ask for it
|
|
afterwards and you get `nil`.
|
|
|
|
The `do` is required and is easy to forget. Leave it out and Lua says
|
|
`'do' expected near`, followed by whatever it found instead, which will be the
|
|
first thing on the next line.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPlot(star * 8, 100)
|
|
----
|
|
|
|
The body. This is what runs forty times, and it is the same line every time --
|
|
what changes is `star`, so the first dot lands at x of 8, the second at 16, and
|
|
the last at 320. A loop is only useful when the body does something with the
|
|
loop's number, and the first thing you will reach for every time is arithmetic
|
|
on it.
|
|
|
|
The body is indented one step further than the `for`, for the same reason
|
|
everything else has been indented so far.
|
|
|
|
[source,lua]
|
|
----
|
|
end
|
|
----
|
|
|
|
Closes the loop. You have now met three things that need an `end`: `function`,
|
|
`if`, and `for`. They all work the same way, they nest inside each other, and
|
|
the indentation is how you keep track of which `end` belongs to what.
|
|
|
|
=== Counting by More Than One
|
|
|
|
The loop counts up by one unless you tell it otherwise. Add a third number and
|
|
it counts by that instead.
|
|
|
|
[source,lua]
|
|
----
|
|
for x = 0, overlayGetWidth() - 1, 10 do
|
|
overlayPlot(x, 100)
|
|
end
|
|
----
|
|
|
|
Read it as *from, to, by*. This one goes 0, 10, 20, and so on, stopping at or
|
|
before the last pixel of the surface, which draws a dotted line all the way
|
|
across whatever size the window happens to be. It is the same row of dots as
|
|
before, described in terms of where they go rather than how many there are.
|
|
|
|
The step can be negative, which counts down.
|
|
|
|
[source,lua]
|
|
----
|
|
for countdown = 10, 1, -1 do
|
|
----
|
|
|
|
Ten, nine, eight, down to one. With a negative step the second number is the
|
|
floor rather than the ceiling, which is what you would expect, and getting the
|
|
sign wrong is a loop that never runs at all: `for i = 10, 1 do` with no step
|
|
counts up from ten towards one, is already past the end before it starts, and
|
|
does nothing. No error, no output. If a loop of yours never seems to run, check
|
|
the direction first.
|
|
|
|
The step can be a fraction too, though you will want that less often than you
|
|
think.
|
|
|
|
=== One Loop Inside Another
|
|
|
|
The body of a loop is ordinary code, and ordinary code can contain a loop.
|
|
|
|
[source,lua]
|
|
----
|
|
for row = 0, overlayGetHeight() - 1, 20 do
|
|
for column = 0, overlayGetWidth() - 1, 20 do
|
|
overlayPlot(column, row)
|
|
end
|
|
end
|
|
----
|
|
|
|
That draws a grid of dots over the whole window. Put it in your
|
|
`onOverlayUpdate` in place of the row and look at it, because the way it runs
|
|
is worth getting straight in your head now rather than in lesson eleven when
|
|
something depends on it.
|
|
|
|
The outer loop runs once for `row` of 0. Inside that, the whole inner loop runs
|
|
from beginning to end -- eighteen columns, eighteen dots, one row of the grid.
|
|
Only when the inner loop has finished does the outer loop move on to `row` of
|
|
20 and run the entire inner loop again.
|
|
|
|
The inner loop's body therefore runs eighteen times eighteen, which is three
|
|
hundred and twenty-four times, for eighteen trips round the outer one. That
|
|
multiplication is why nested loops are how you fill a rectangle with anything,
|
|
and also why you should glance at the numbers before you nest three of them.
|
|
|
|
Notice that each dot's position needs both loop variables: `column` for the
|
|
across and `row` for the down. That is the pattern. If the inner body only uses
|
|
the inner variable, the inner loop is drawing the same thing over and over in
|
|
the same place, and you have written a slow way of doing it once.
|
|
|
|
=== When You Do Not Know How Many
|
|
|
|
A `for` loop needs to know how many times before it starts. Sometimes you do
|
|
not, and then you want the other kind.
|
|
|
|
[source,lua]
|
|
----
|
|
x = 0
|
|
while x < overlayGetWidth() do
|
|
overlayPlot(x, 100)
|
|
x = x + 10
|
|
end
|
|
----
|
|
|
|
A `while` loop checks a question before every trip round, exactly the question
|
|
an `if` would ask, and keeps going for as long as the answer is true. When the
|
|
answer is false it stops and carries on with the line after the `end`. If the
|
|
answer is false the very first time, the body never runs at all.
|
|
|
|
That example draws the same dotted line as the `for` with a step of ten, and it
|
|
takes three lines to do what `for` did in one, so use `for` for that. Here is
|
|
the difference that decides it:
|
|
|
|
* Use `for` when the number of trips is known before you start. Forty stars.
|
|
Every tenth pixel across. Every row of a grid.
|
|
* Use `while` when you are waiting for something to become true and cannot say
|
|
in advance how long it takes. Deal cards until the deck runs out. Keep asking
|
|
until the player types something valid. Step through a list until you find
|
|
what you were looking for.
|
|
|
|
A `while` has three parts and you have to write all three yourself: set the
|
|
variable up before the loop, test it in the `while`, and change it inside the
|
|
body. The `for` loop does all three for you, which is why it is the one to
|
|
reach for when it fits. Forget the third part in a `while` and the question
|
|
never stops being true, which you will do on purpose at the end of this lesson.
|
|
|
|
=== A Star Made Out of Its Own Number
|
|
|
|
Now the star field, and a problem that shapes the rest of part one.
|
|
|
|
Each star needs an x and a y of its own, and you have no way to keep a hundred
|
|
separate variables -- writing `star1X`, `star2X` and so on up to a hundred is
|
|
worse than typing the hundred lines you were avoiding. So do not store them.
|
|
Work each one out from the only thing that makes a star different from its
|
|
neighbours: its number.
|
|
|
|
[source,lua]
|
|
----
|
|
starX = (star * 37) % width
|
|
----
|
|
|
|
The `%` is the last of the arithmetic operators and it is the *remainder*: what
|
|
is left over after dividing. Fifteen divided by four is three with three left
|
|
over, so `15 % 4` is 3. Its useful property is that the answer can never be as
|
|
large as the number on the right, so a remainder by `width` is always a
|
|
position somewhere on the screen no matter how big the left-hand side gets.
|
|
|
|
Star 1 goes to 37, star 5 to 185, star 10 to 10 -- 370 wrapped round past 360
|
|
and came back at the left. Thirty-seven is chosen because it shares no factor
|
|
with 360, which is what stops the stars landing in neat stripes; try 36 instead
|
|
and you will see the problem immediately.
|
|
|
|
The downward drift is the same trick with a number that grows over time.
|
|
|
|
[source,lua]
|
|
----
|
|
drift = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
...
|
|
starY = (star * 61 + math.floor(drift * layer * 0.5)) % height
|
|
----
|
|
|
|
`drift` lives outside the function and climbs every frame, exactly like
|
|
`frames` in lesson two. Adding it to every star's y moves the whole field down
|
|
together, and the `%` brings a star that falls off the bottom back on at the
|
|
top, forever, for free.
|
|
|
|
`math.floor` throws away the fraction of a number and hands back the whole part
|
|
below it: `math.floor(7.9)` is 7. Lesson two pointed out that Singe does that
|
|
for you when it draws, and it does, but doing it yourself means the number you
|
|
are working with is the number that appears on screen. That will matter in
|
|
lesson seven, when two things have to agree about whether they are touching.
|
|
|
|
Note that `layer` in there is a second loop variable. The stars are drawn by a
|
|
loop inside a loop, and the outer one is not a row of a grid but a *distance*:
|
|
three layers of stars, each drifting at its own speed and its own brightness,
|
|
which is what makes a flat field of dots look like it has depth.
|
|
|
|
=== Chance
|
|
|
|
Stars twinkle. Twinkling means a brightness that is a bit different every
|
|
frame, and "a bit different" is `math.random`.
|
|
|
|
[source,lua]
|
|
----
|
|
shade = math.random(60, 90) + layer * 55
|
|
colorForeground(shade, shade, shade)
|
|
----
|
|
|
|
`math.random(60, 90)` hands back a whole number from 60 to 90, both included,
|
|
chosen afresh every time you call it. Call it with no arguments at all and you
|
|
get a fraction between 0 and 1 instead, which is the form to use when you want
|
|
something to happen one time in five. You do not have to set the generator up;
|
|
Lua does that when Singe starts.
|
|
|
|
Adding `layer * 55` lifts the near layers out of the dim range, so the far
|
|
stars stay faint and the near ones are almost white. Equal red, green, and
|
|
blue, as lesson three explained, is a shade of grey.
|
|
|
|
Because `shade` is picked again on every frame for every star, each star
|
|
flickers on its own, which at sixty frames a second reads as a twinkle rather
|
|
than as a fault.
|
|
|
|
=== The Whole Thing
|
|
|
|
[source,lua]
|
|
----
|
|
LAYERS = 3
|
|
STARS_PER_LAYER = 40
|
|
ACROSS_STEP = 37
|
|
DOWN_STEP = 61
|
|
LAYER_SHIFT = 13
|
|
LAYER_SPEED = 0.5
|
|
LAYER_BRIGHT = 55
|
|
|
|
width = overlayGetWidth()
|
|
height = overlayGetHeight()
|
|
|
|
drift = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
overlayClear()
|
|
|
|
for layer = 1, LAYERS do
|
|
for star = 1, STARS_PER_LAYER do
|
|
starX = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
|
starY = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
|
shade = math.random(60, 90) + layer * LAYER_BRIGHT
|
|
colorForeground(shade, shade, shade)
|
|
overlayPlot(starX, starY)
|
|
end
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
A hundred and twenty stars, drawn by five lines, and the file is shorter than
|
|
the one in lesson three.
|
|
|
|
It is also as far as this approach goes, and it is worth being honest about why
|
|
while you are looking at it. Every star in there is a formula. Nothing about a
|
|
star is *remembered*: you cannot make one brighter than its neighbour and have
|
|
it stay that way, you cannot knock one out of the sky, and you cannot have the
|
|
player collect one, because there is nowhere to write down that anything
|
|
happened to it. Star seventeen is not a thing. It is a number that briefly
|
|
passes through a variable called `star`.
|
|
|
|
The moment you want a hundred things that each remember something of their own,
|
|
you need somewhere to put a hundred sets of facts. That is lesson six, and the
|
|
star field is why it exists.
|
|
|
|
=== Try It
|
|
|
|
. *More and fewer.* Try `STARS_PER_LAYER = 200`, then `4`. Then `LAYERS = 8`
|
|
and watch what `layer * LAYER_BRIGHT` does to `shade` when the layer number
|
|
gets big. The numbers `colorForeground` takes stop at 255.
|
|
. *Change the spacing.* Set `ACROSS_STEP` to 36 and run it. The stars fall into
|
|
stripes, because 36 divides into 360 exactly ten times. Put it back to 37 and
|
|
try 71 and 90.
|
|
. *Stop the twinkle.* Replace the `math.random` line with a plain
|
|
`shade = layer * 70` and compare. Decide which you prefer; there is no right
|
|
answer, and noticing that you have a preference is part of the job.
|
|
. *Make them fall upward.* One character.
|
|
. *Draw the grid as well.* Put the nested grid loop from earlier back in, after
|
|
the stars, in a dim colour. Two nested loops in one function, drawing two
|
|
different things, and the second one does not disturb the first.
|
|
|
|
=== Break It on Purpose
|
|
|
|
Every error so far has been a message. This one is not, and that is exactly why
|
|
you should meet it while you are expecting it.
|
|
|
|
Put this in your `onOverlayUpdate`, run it, and read the next paragraph before
|
|
you do anything else.
|
|
|
|
[source,lua]
|
|
----
|
|
x = 0
|
|
while x < width do
|
|
overlayPlot(x, 100)
|
|
end
|
|
----
|
|
|
|
The line that moves `x` along is missing, so `x` stays at zero, so `x < width`
|
|
never stops being true. The loop goes round for ever, plotting the same pixel,
|
|
and it is inside `onOverlayUpdate`, so `onOverlayUpdate` never returns. Singe
|
|
is waiting for your function. It cannot draw, it cannot read the keyboard, and
|
|
it cannot quit.
|
|
|
|
What you see is a window that stops updating and stops responding. Escape does
|
|
nothing. Q does nothing. Your desktop may grey the window out or offer to force
|
|
it closed, and it may say the program is not responding, which is true.
|
|
|
|
Go to the terminal you started Singe in and press *Ctrl* and *C* together. That
|
|
kills it. Then take the loop out and save.
|
|
|
|
There is no message because nothing went wrong, as far as the computer is
|
|
concerned. You asked for a loop that runs while `x` is less than the width, and
|
|
that is precisely what you got. This is the one kind of mistake the error
|
|
messages cannot help you with, and the cure is a habit: every time you write a
|
|
`while`, write the line that changes the variable before you write anything
|
|
else in the body.
|
|
|
|
=== What You Learned
|
|
|
|
* `for name = first, last do ... end` runs its body once for each number from
|
|
first to last, both included.
|
|
* A third number is the step, and it may be negative to count down.
|
|
* The loop variable exists only inside the loop, and the body should use it.
|
|
* `for` needs a `do`, and every loop needs an `end`.
|
|
* A loop inside a loop runs the whole inner loop for each trip round the outer
|
|
one, which is how you fill a rectangle.
|
|
* `while question do ... end` repeats for as long as the question is true, and
|
|
you have to change something in the body yourself.
|
|
* Use `for` when you know how many times, and `while` when you are waiting for
|
|
something to become true.
|
|
* `%` is the remainder, and it keeps a growing number inside a range.
|
|
* `math.random(a, b)` gives a whole number from a to b; `math.floor` throws away
|
|
the fraction.
|
|
* A loop that never ends freezes the whole game with no error message, and
|
|
*Ctrl* and *C* in the terminal is how you get out.
|
|
|
|
=== Next Time
|
|
|
|
Look at that `onOverlayUpdate` again. It draws stars, it works out positions, it
|
|
picks colours, and it is starting to be a function that does four jobs instead
|
|
of one. In lesson five you write functions of your own, give them arguments the
|
|
way Singe's functions take them, and get answers back out of them -- and the
|
|
star field becomes one line that says `drawStars()`.
|