singe/docs/de/lessons/04-repeating.adoc

401 lines
17 KiB
Text

[#_lesson_4_doing_it_again]
== Lektion 4: Noch einmal
image::learn/04-repeating.png[Die fertige Lektion, 480]
Du hast ein Wort und ein Kästchen gezeichnet. Ein Spiel braucht hundert Dinge: Sterne,
Steine, Geschosse, die Zeilen eines Menüs. Hundert Zeilen abzutippen ist nicht die Antwort, und
wenn es das wäre, müsstest du noch einmal hundert tippen, sobald du hunderteins willst.
Diese Lektion ist die Antwort. Am Ende treibt ein Sternenfeld dein Fenster hinunter, und jeder
einzelne Stern wird von denselben fünf Zeilen Code gezeichnet.
[#_start_a_new_folder_3]
=== Leg einen neuen Ordner an
Ein Ordner namens `repeating`, darin eine Datei namens `repeating.singe`, und in einem Terminal
läuft `Singe -R repeating`.
[#_forty_dots]
=== Vierzig Punkte
Tipp das ein und starte es.
[source,lua]
----
function onOverlayUpdate()
overlayClear()
colorForeground(255, 255, 255)
for star = 1, 40 do
overlayPlot(star * 8, 100)
end
return OVERLAY_UPDATED
end
----
Eine Reihe aus vierzig weißen Punkten quer durch die Mitte des Fensters.
`overlayPlot` ist der einfachste Zeichenaufruf, den es gibt: Er setzt ein Pixel auf die aktuelle
Vordergrundfarbe, und genau das will ein Stern. Alles Interessante hier steckt in den drei Zeilen
drumherum.
[source,lua]
----
for star = 1, 40 do
----
Das sagt: Tu das Folgende vierzigmal, und bei jedem Durchlauf steht in `star` eine andere Zahl --
eins beim ersten Mal, zwei beim zweiten, vierzig beim letzten. Beide Enden zählen mit, also sind
`1, 40` wirklich vierzig Durchläufe und nicht neununddreißig. Diese Form ist eine *Schleife*, und
diese hier ist eine *numerische for-Schleife*.
`star` ist ein Name, den du dir ausdenkst, so wie du dir in Lektion zwei `frames` ausgedacht hast,
und die Schleife füllt ihn für dich. Du setzt ihn nicht, und du solltest ihn in der Schleife nicht
ändern. Es gibt ihn nur zwischen dem `for` und seinem `end`; fragst du danach nach ihm, bekommst
du `nil`.
Das `do` ist Pflicht und leicht zu vergessen. Lässt du es weg, sagt Lua
`'do' expected near`, gefolgt von dem, was es stattdessen gefunden hat, und das ist das Erste in
der nächsten Zeile.
[source,lua]
----
overlayPlot(star * 8, 100)
----
Der Rumpf. Das ist es, was vierzigmal läuft, und es ist jedes Mal dieselbe Zeile -- was sich
ändert, ist `star`, also landet der erste Punkt bei x gleich 8, der zweite bei 16 und der letzte
bei 320. Eine Schleife ist nur nützlich, wenn der Rumpf etwas mit der Nummer der Schleife macht,
und das Erste, wozu du jedes Mal greifen wirst, ist, damit zu rechnen.
Der Rumpf ist eine Stufe weiter eingerückt als das `for`, aus demselben Grund, aus dem bisher alles
andere eingerückt war.
[source,lua]
----
end
----
Schließt die Schleife. Jetzt kennst du drei Dinge, die ein `end` brauchen: `function`, `if` und
`for`. Sie funktionieren alle gleich, sie lassen sich ineinander verschachteln, und an der
Einrückung behältst du den Überblick, welches `end` wozu gehört.
[#_counting_by_more_than_one]
=== In größeren Schritten zählen
Die Schleife zählt in Einerschritten hoch, wenn du ihr nichts anderes sagst. Füg eine dritte Zahl
hinzu, und sie zählt stattdessen in diesen Schritten.
[source,lua]
----
for x = 0, overlayGetWidth() - 1, 10 do
overlayPlot(x, 100)
end
----
Lies es als *von, bis, Schrittweite*. Diese hier geht 0, 10, 20 und so weiter und hört beim letzten
Pixel der Fläche oder davor auf, und so zeichnet sie eine gepunktete Linie ganz quer hinüber, egal
wie groß das Fenster gerade ist. Es ist dieselbe Punktreihe wie vorher, beschrieben danach, wo die
Punkte hinkommen, statt wie viele es sind.
Die Schrittweite kann negativ sein, dann zählt die Schleife rückwärts.
[source,lua]
----
for countdown = 10, 1, -1 do
----
Zehn, neun, acht, bis hinunter zu eins. Mit einer negativen Schrittweite ist die zweite Zahl der
Boden statt der Decke, was du wohl auch erwartest, und ein falsches Vorzeichen ergibt eine
Schleife, die nie läuft: `for i = 10, 1 do` ohne Schrittweite zählt von zehn aufwärts in Richtung
eins, ist schon hinter dem Ende, bevor es losgeht, und tut nichts. Kein Fehler, keine Ausgabe. Wenn
eine deiner Schleifen nie zu laufen scheint, prüf zuerst die Richtung.
Die Schrittweite kann auch ein Bruch sein, aber das brauchst du seltener, als du denkst.
[#_one_loop_inside_another]
=== Eine Schleife in einer anderen
Der Rumpf einer Schleife ist gewöhnlicher Code, und gewöhnlicher Code kann eine Schleife enthalten.
[source,lua]
----
for row = 0, overlayGetHeight() - 1, 20 do
for column = 0, overlayGetWidth() - 1, 20 do
overlayPlot(column, row)
end
end
----
Das zeichnet ein Punktraster über das ganze Fenster. Setz es in deinem `onOverlayUpdate` anstelle
der Reihe ein und sieh es dir an, denn wie es abläuft, solltest du dir jetzt klarmachen und nicht
erst in Lektion elf, wenn etwas davon abhängt.
Die äußere Schleife läuft einmal mit `row` gleich 0. Darin läuft die ganze innere Schleife von
Anfang bis Ende -- achtzehn Spalten, achtzehn Punkte, eine Zeile des Rasters. Erst wenn die innere
Schleife fertig ist, geht die äußere weiter zu `row` gleich 20 und lässt die ganze innere Schleife
noch einmal laufen.
Der Rumpf der inneren Schleife läuft also achtzehn mal zwölf, das sind zweihundertsechzehn
Mal, bei zwölf Durchläufen der äußeren. Wegen dieser Multiplikation füllst du mit
verschachtelten Schleifen ein Rechteck mit was auch immer, und darum solltest du auch einen Blick
auf die Zahlen werfen, bevor du drei davon verschachtelst.
Beachte, dass die Position jedes Punkts beide Schleifenvariablen braucht: `column` für waagerecht
und `row` für senkrecht. Das ist das Muster. Wenn der innere Rumpf nur die innere Variable benutzt,
zeichnet die innere Schleife immer wieder dasselbe an dieselbe Stelle, und du hast eine langsame
Art geschrieben, es einmal zu tun.
[#_when_you_do_not_know_how_many]
=== Wenn du nicht weißt, wie viele
Eine `for`-Schleife muss wissen, wie oft, bevor sie anfängt. Manchmal weißt du das nicht, und dann
willst du die andere Sorte.
[source,lua]
----
x = 0
while x < overlayGetWidth() do
overlayPlot(x, 100)
x = x + 10
end
----
Eine `while`-Schleife prüft vor jedem Durchlauf eine Frage, genau so eine Frage, wie ein `if` sie
stellen würde, und macht weiter, solange die Antwort wahr ist. Ist die Antwort falsch, hört sie auf
und macht mit der Zeile nach dem `end` weiter. Ist die Antwort gleich beim ersten Mal falsch, läuft
der Rumpf überhaupt nie.
Dieses Beispiel zeichnet dieselbe gepunktete Linie wie das `for` mit Schrittweite zehn, und es
braucht drei Zeilen für das, was `for` in einer erledigt hat, also nimm dafür `for`. Hier ist der
Unterschied, der es entscheidet:
* Nimm `for`, wenn die Zahl der Durchläufe feststeht, bevor du anfängst. Vierzig Sterne. Jedes
zehnte Pixel quer hinüber. Jede Zeile eines Rasters.
* Nimm `while`, wenn du darauf wartest, dass etwas wahr wird, und nicht vorher sagen kannst, wie
lange das dauert. Karten austeilen, bis der Stapel leer ist. Immer wieder fragen, bis der Spieler
etwas Gültiges eintippt. Eine Liste durchgehen, bis du findest, was du gesucht hast.
Ein `while` hat drei Teile, und alle drei musst du selbst schreiben: die Variable vor der Schleife
anlegen, sie im `while` prüfen und sie im Rumpf ändern. Die `for`-Schleife erledigt alle drei für
dich, und darum greifst du zu ihr, wenn sie passt. Vergiss im `while` den dritten Teil, und die
Frage hört nie auf, wahr zu sein; genau das machst du am Ende dieser Lektion absichtlich.
[#_a_star_made_out_of_its_own_number]
=== Ein Stern aus seiner eigenen Nummer
Jetzt das Sternenfeld und ein Problem, das den Rest von Teil eins prägt.
Jeder Stern braucht ein eigenes x und ein eigenes y, und du hast keine Möglichkeit, hundert
getrennte Variablen aufzubewahren -- `star1X`, `star2X` und so weiter bis hundert zu schreiben ist
schlimmer, als die hundert Zeilen abzutippen, um die du herumkommen wolltest. Also speichere sie
nicht. Rechne jede Position aus dem Einzigen aus, was einen Stern von seinen Nachbarn
unterscheidet: seiner Nummer.
[source,lua]
----
starX = (star * 37) % width
----
Das `%` ist der letzte der Rechenoperatoren, und es ist der *Rest*: das, was beim Teilen übrig
bleibt. Fünfzehn geteilt durch vier ist drei, Rest drei, also ist `15 % 4` gleich 3. Seine
nützliche Eigenschaft ist, dass das Ergebnis nie so groß werden kann wie die Zahl rechts, also ist
der Rest beim Teilen durch `width` immer eine Position irgendwo auf dem Bildschirm, ganz egal, wie
groß die linke Seite wird.
Stern 1 landet bei 37, Stern 5 bei 185, Stern 10 bei 10 -- 370 ist über 360 hinausgelaufen und
links wieder hereingekommen. Siebenunddreißig ist gewählt, weil es keinen gemeinsamen Teiler mit 360
hat, und das verhindert, dass die Sterne in ordentlichen Streifen landen; probier stattdessen 36,
und du siehst das Problem sofort.
Das Hinabtreiben ist derselbe Trick mit einer Zahl, die mit der Zeit wächst.
[source,lua]
----
drift = 0
function onOverlayUpdate()
drift = drift + 1
...
starY = (star * 61 + math.floor(drift * layer * 0.5)) % height
----
`drift` wohnt außerhalb der Funktion und steigt in jedem Frame, genau wie `frames` in Lektion zwei.
Wenn du es zum y jedes Sterns addierst, bewegt sich das ganze Feld gemeinsam nach unten, und das
`%` holt einen Stern, der unten herausfällt, oben wieder herein, für immer, gratis.
`math.floor` wirft den Bruchteil einer Zahl weg und gibt den ganzen Teil darunter zurück:
`math.floor(7.9)` ist 7. In Lektion zwei hieß es, dass Singe das beim Zeichnen für dich tut, und
das stimmt auch, aber wenn du es selbst tust, ist die Zahl, mit der du arbeitest, die Zahl, die auf
dem Bildschirm erscheint. Das wird in Lektion sieben wichtig, wenn sich zwei Dinge einig sein
müssen, ob sie sich berühren.
Beachte, dass `layer` darin eine zweite Schleifenvariable ist. Die Sterne werden von einer Schleife
in einer Schleife gezeichnet, und die äußere ist keine Zeile eines Rasters, sondern eine
*Entfernung*: drei Ebenen von Sternen, jede mit eigener Geschwindigkeit und eigener Helligkeit, und
das lässt ein flaches Punktfeld aussehen, als hätte es Tiefe.
[#_chance]
=== Zufall
Sterne funkeln. Funkeln heißt eine Helligkeit, die in jedem Frame ein bisschen anders ist, und „ein
bisschen anders“ ist `math.random`.
[source,lua]
----
shade = math.random(60, 90) + layer * 55
colorForeground(shade, shade, shade)
----
`math.random(60, 90)` gibt eine ganze Zahl von 60 bis 90 zurück, beide eingeschlossen, bei jedem
Aufruf neu gewählt. Rufst du es ganz ohne Argumente auf, bekommst du stattdessen einen Bruch
zwischen 0 und 1, und diese Form nimmst du, wenn etwas in einem von fünf Fällen passieren soll. Den
Zufallsgenerator musst du nicht einrichten; das macht Lua, wenn Singe startet.
Das Addieren von `layer * 55` hebt die nahen Ebenen aus dem trüben Bereich heraus, also bleiben die
fernen Sterne schwach und die nahen sind fast weiß. Gleich viel Rot, Grün und Blau ist, wie
Lektion drei erklärt hat, ein Grauton.
Weil `shade` in jedem Frame für jeden Stern neu gewählt wird, flackert jeder Stern für sich, und
bei sechzig Frames pro Sekunde wirkt das wie Funkeln und nicht wie ein Fehler.
[#_the_whole_thing_3]
=== Das Ganze
[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
----
Hundertzwanzig Sterne, gezeichnet von fünf Zeilen, und die Datei ist kürzer als die aus Lektion
drei.
Weiter kommst du mit diesem Ansatz aber auch nicht, und es lohnt sich, ehrlich zu sagen, warum,
solange du es vor dir hast. Jeder Stern darin ist eine Formel. Nichts an einem Stern wird
*gemerkt*: Du kannst keinen heller machen als seinen Nachbarn und ihn so bleiben lassen, du kannst
keinen vom Himmel schießen, und du kannst keinen vom Spieler einsammeln lassen, weil es keinen Ort
gibt, an dem du aufschreiben könntest, dass ihm etwas passiert ist. Stern siebzehn ist kein Ding.
Er ist eine Zahl, die kurz durch eine Variable namens `star` wandert.
Sobald du hundert Dinge willst, die sich jedes etwas Eigenes merken, brauchst du einen Ort für
hundert Sätze von Fakten. Das ist Lektion sechs, und das Sternenfeld ist der Grund, warum es sie
gibt.
[#_try_it_4]
=== Probier es aus
. *Mehr und weniger.* Probier `STARS_PER_LAYER = 200`, dann `4`. Dann `LAYERS = 8`, und sieh dir
an, was `layer * LAYER_BRIGHT` mit `shade` macht, wenn die Nummer der Ebene groß wird. Die
Zahlen, die `colorForeground` annimmt, hören bei 255 auf.
. *Ändere die Abstände.* Setz `ACROSS_STEP` auf 36 und starte es. Die Sterne ordnen sich in
Streifen, weil 36 genau zehnmal in 360 passt. Stell es wieder auf 37 und probier 71 und 90.
. *Stopp das Funkeln.* Ersetze die Zeile mit `math.random` durch ein schlichtes
`shade = layer * 70` und vergleiche. Entscheide, was dir besser gefällt; es gibt keine richtige
Antwort, und zu bemerken, dass du einen Geschmack hast, gehört zur Arbeit.
. *Lass sie nach oben fallen.* Ein Zeichen.
. *Zeichne auch das Raster.* Setz die verschachtelte Rasterschleife von vorhin wieder ein, nach den
Sternen, in einer gedämpften Farbe. Zwei verschachtelte Schleifen in einer Funktion, die zwei
verschiedene Dinge zeichnen, und die zweite stört die erste nicht.
[#_break_it_on_purpose_4]
=== Mach es absichtlich kaputt
Jeder Fehler bisher war eine Meldung. Dieser hier nicht, und genau darum solltest du ihn
kennenlernen, während du mit ihm rechnest.
Setz das in dein `onOverlayUpdate`, direkt unter `overlayClear()`, starte es und lies den
nächsten Absatz, bevor du irgendetwas anderes tust.
[source,lua]
----
x = 0
while x < width do
overlayPlot(x, 100)
end
----
Die Zeile, die `x` weiterschiebt, fehlt, also bleibt `x` auf null, also hört `x < width` nie auf,
wahr zu sein. Die Schleife dreht sich für immer und setzt immer dasselbe Pixel, und sie steckt in
`onOverlayUpdate`, also kehrt `onOverlayUpdate` nie zurück. Singe wartet auf deine Funktion. Es
kann nicht zeichnen, es kann die Tastatur nicht lesen, und es kann sich nicht beenden.
Was du siehst, ist ein Fenster, in dem sich nichts mehr bewegt. Esc bewirkt nichts. Q bewirkt
nichts. Ausgegeben wird auch nichts.
Geh in das Terminal, in dem du Singe gestartet hast, und drück *Strg* und *C* zusammen. Singe
hält deine Schleife an und sagt dir, wo sie gerade war:
----
Stopped: the script did not return, and Singe was asked to quit.
stack traceback:
repeating/repeating.singe:20: in function 'onOverlayUpdate'
----
Vielleicht sagt es das zweimal, ein paar Sekunden auseinander, bevor es sich beendet. Einmal
reicht völlig. Zeile 20 ist das `end` deines `while`, und dort drehte sich die Schleife, als du
sie angehalten hast. Nimm die Schleife heraus, speichere und starte Singe neu.
Das ist ein Bericht, kein Fehler. Nichts hat sich beschwert, bis du gefragt hast, weil aus Sicht
des Computers nichts schiefgegangen ist. Du hast eine Schleife verlangt, die läuft, solange `x` kleiner als die Breite ist, und genau die hast du
bekommen. Das ist die eine Art von Fehler, bei der dir die Fehlermeldungen nicht helfen können, und
das Gegenmittel ist eine Gewohnheit: Jedes Mal, wenn du ein `while` schreibst, schreib die Zeile,
die die Variable ändert, bevor du irgendetwas anderes in den Rumpf schreibst.
[#_what_you_learned_4]
=== Was du gelernt hast
* `for name = first, last do ... end` führt seinen Rumpf einmal für jede Zahl vom ersten bis zum
letzten Wert aus, beide eingeschlossen.
* Eine dritte Zahl ist die Schrittweite, und sie darf negativ sein, um rückwärts zu zählen.
* Die Schleifenvariable gibt es nur in der Schleife, und der Rumpf sollte sie benutzen.
* `for` braucht ein `do`, und jede Schleife braucht ein `end`.
* Eine Schleife in einer Schleife lässt bei jedem Durchlauf der äußeren die ganze innere laufen,
und so füllst du ein Rechteck.
* `while question do ... end` wiederholt, solange die Frage wahr ist, und du musst selbst im Rumpf
etwas ändern.
* Nimm `for`, wenn du weißt, wie oft, und `while`, wenn du darauf wartest, dass etwas wahr wird.
* `%` ist der Rest, und er hält eine wachsende Zahl in einem Bereich.
* `math.random(a, b)` liefert eine ganze Zahl von a bis b; `math.floor` wirft den Bruchteil weg.
* Eine Schleife, die nie endet, friert das ganze Spiel ohne Fehlermeldung ein, und mit *Strg* und
*C* im Terminal kommst du wieder heraus.
[#_next_time_4]
=== Beim nächsten Mal
Sieh dir dieses `onOverlayUpdate` noch einmal an. Es zeichnet Sterne, es rechnet Positionen aus, es
wählt Farben, und es wird langsam zu einer Funktion, die vier Aufgaben erledigt statt einer. In
Lektion fünf schreibst du eigene Funktionen, gibst ihnen Argumente, so wie Singes Funktionen sie
bekommen, und holst Antworten aus ihnen heraus -- und das Sternenfeld wird zu einer Zeile, in der
`drawStars()` steht.