401 lines
17 KiB
Text
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.
|