singe/docs/lessons/04-repeating.adoc
2026-09-22 21:57:42 -05:00

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()`.