403 lines
16 KiB
Text
403 lines
16 KiB
Text
[#_lesson_2_numbers_that_change]
|
|
== Lektion 2: Zahlen, die sich ändern
|
|
|
|
image::learn/02-numbers.png[Die fertige Lektion, 480]
|
|
|
|
In Lektion eins hast du ein Wort auf den Bildschirm gebracht, und da saß es dann. Am Ende dieser
|
|
Lektion gleitet dasselbe Wort durchs Fenster, kehrt um, wenn es an einen Rand kommt, und das tut
|
|
es wegen vier Zahlen, die das Programm sechzigmal pro Sekunde ändert.
|
|
|
|
Unterwegs triffst du auf die eine Idee, über die mehr Anfänger stolpern als über jede andere: Wo
|
|
du etwas hinschreibst, entscheidet, wie lange es lebt.
|
|
|
|
[#_start_a_new_folder]
|
|
=== Leg einen neuen Ordner an
|
|
|
|
Leg neben deinem Ordner `hello` einen Ordner namens `numbers` an und darin eine Datei namens
|
|
`numbers.singe`. Tipp das Programm aus Lektion eins hinein, oder kopier `hello.singe` herüber und
|
|
benenne sie um -- beides ist in Ordnung, und das Abtippen ist keine verschwendete Zeit.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Starte es mit `Singe -R numbers` und lass es laufen. Dank dem `-R` vom Ende der Lektion eins
|
|
musst du es für den Rest dieser Lektion nicht wieder starten; speichere die Datei, und das Fenster
|
|
zieht von selbst nach.
|
|
|
|
[#_count_the_frames]
|
|
=== Zähl die Frames
|
|
|
|
In Lektion eins hieß es, dass Singe `onOverlayUpdate` etwa sechzigmal pro Sekunde aufruft. Das ist
|
|
leicht gesagt und schwer zu glauben, also zähl nach.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
frames = 0
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Speichere. Oben links in der Ecke steht `frame 1`, und es bleibt für immer bei `frame 1`.
|
|
|
|
Das ist enttäuschend, und es ist zugleich das Nützlichste in dieser Lektion, also reparier es noch
|
|
nicht. Sieh dir an, was du geschrieben hast, und denk über die Reihenfolge der Zeilen nach, die laut
|
|
Lektion eins das Wichtigste an einem Programm ist.
|
|
|
|
Singe ruft `onOverlayUpdate` auf. Die erste Zeile setzt `frames` auf null. Die zweite zählt eins
|
|
dazu, macht eins. Du gibst es aus, du kehrst zurück, und die Funktion ist vorbei. Dann ruft Singe
|
|
sie wieder auf, und als Erstes wird `frames` wieder auf null gesetzt.
|
|
|
|
Du zählst keine Frames. Du zählst bis eins, sechzigmal pro Sekunde.
|
|
|
|
[#_move_the_line_out]
|
|
=== Hol die Zeile heraus
|
|
|
|
Nimm die Zeile `frames = 0` aus der Funktion heraus und setz sie darüber, für sich allein, vor das
|
|
Wort `function`.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Speichere. Die Zahl steigt, schnell, und hört nicht auf.
|
|
|
|
Sonst hat sich nichts geändert. Der einzige Unterschied ist, *wo* die Zeile steht, und das ist
|
|
schon die ganze Idee. Zeilen in einer Funktion passieren jedes einzelne Mal, wenn die Funktion
|
|
aufgerufen wird. Zeilen außerhalb jeder Funktion passieren einmal, wenn Singe deine Datei liest,
|
|
bevor das Spiel anfängt.
|
|
|
|
`frames = 0` musste einmal passieren. Es passierte sechzigmal pro Sekunde und machte die Arbeit der
|
|
Zeile darunter zunichte.
|
|
|
|
Alles, was sich bewegt und was du je schreibst, hat diese Form. Die Zahl draußen anlegen, drinnen
|
|
ändern.
|
|
|
|
[#_what_just_happened_2]
|
|
=== Was gerade passiert ist
|
|
|
|
Da drin stecken drei neue Stücke Code. Hier sind sie einzeln.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
----
|
|
|
|
Das denkt sich einen Namen aus, `frames`, und legt die Zahl null dahinter. Ein Name mit einem Wert
|
|
dahinter ist eine *Variable*, und sie heißt so, weil der Wert variieren darf -- sich ändern,
|
|
während das Programm läuft.
|
|
|
|
Das `=` ist nicht das Gleichheitszeichen aus der Schule. Es sagt nicht, dass `frames` gleich null
|
|
ist; es sagt *leg null in `frames`*, und es arbeitet immer in diese Richtung, von rechts nach links.
|
|
Programmierer nennen das *Zuweisung*, und es lohnt sich, die Zeile beim Schreiben so laut
|
|
auszusprechen: „frames bekommt null“.
|
|
|
|
Den Namen wählst du. Er kann fast alles sein, was mit einem Buchstaben anfängt und keine
|
|
Leerzeichen enthält, und er sollte sagen, was das Ding ist. `frames` ist ein guter Name. `f` ist
|
|
ein schlechter, und in einer Woche wirst du dir dafür nicht dankbar sein.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = frames + 1
|
|
----
|
|
|
|
Das ist die Zeile, bei der die Leute innehalten und starren, denn als Aussage übers Rechnen ist sie
|
|
Unsinn: Nichts ist gleich sich selbst plus eins.
|
|
|
|
Sie ist keine Aussage übers Rechnen. Denk daran, dass `=` die rechte Seite in den Namen links legt,
|
|
und lies sie in dieser Reihenfolge, rechts zuerst. Rechne `frames + 1` aus, mit dem, was gerade in
|
|
`frames` steht. Dann leg das Ergebnis zurück in `frames`. Stand in `frames` eine Sechs, ergibt die
|
|
rechte Seite sieben, und sieben kommt in `frames`.
|
|
|
|
Eins zu einer Variablen dazuzuzählen wie hier wirst du tausende Male schreiben.
|
|
|
|
Das `+` tut, was du erwartest, und die anderen drei auch: `-` subtrahiert, `*` multipliziert und
|
|
`/` dividiert. Es gibt keine Taste `x` zum Malnehmen, darum übernimmt das der Stern.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
----
|
|
|
|
`overlayPrint` will Text, und `frames` ist eine Zahl, kein Text. Die zwei Punkte fügen die beiden
|
|
zusammen und übergeben das Ergebnis als einen String: die fünf Zeichen `frame`, ein Leerzeichen
|
|
und dann so viele Ziffern, wie die Zahl braucht.
|
|
|
|
Diese zwei Punkte sind der Verbindungsoperator, und es lohnt sich, seinen richtigen Namen zu
|
|
kennen, weil die Fehlermeldungen ihn benutzen: Strings aneinanderzuhängen heißt *Konkatenation*.
|
|
|
|
Beachte das Leerzeichen in den Anführungszeichen, vor dem schließenden. Ohne es bekommst du
|
|
`frame6`. Lua hängt genau das aneinander, was du ihm gibst, und tut nichts Eigenes dazu.
|
|
|
|
Lua macht für dich aus einer Zahl Text, wenn du sie so an einen String hängst, und darum musstest
|
|
du nichts Besonderes tun. Das macht es nicht mit allem. Wenn du je versuchst, etwas anzuhängen, das
|
|
weder Text noch Zahl ist, hält Lua mit einem Fehler an, und der Weg drumherum ist `tostring`, das
|
|
aus allem überhaupt Text macht:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. tostring(frames))
|
|
----
|
|
|
|
Diese Zeile tut genau dasselbe wie die davor. Für Zahlen brauchst du `tostring` nicht. In Lektion
|
|
drei wirst du es brauchen.
|
|
|
|
[#_make_it_move]
|
|
=== Bring es in Bewegung
|
|
|
|
Jetzt hast du alles, was du brauchst, um das Wort zu bewegen. Das Wort wird gezeichnet, wo du
|
|
willst, du hast eine Möglichkeit, eine Zahl von einem Frame zum nächsten aufzubewahren, und du
|
|
hast eine Möglichkeit, sie zu ändern.
|
|
|
|
Gib der Spalte eine eigene Variable außerhalb der Funktion und zähl drinnen etwas dazu.
|
|
|
|
[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
|
|
----
|
|
|
|
Speichere. Das Wort schießt rechts hinaus und kommt nie zurück.
|
|
|
|
Es ist in jedem Frame eine Spalte weitergerückt, sechzigmal pro Sekunde, also hat es das ganze
|
|
Fenster in etwa einer Sekunde durchquert. Bremse es, indem du weniger als eine ganze Spalte
|
|
dazuzählst:
|
|
|
|
[source,lua]
|
|
----
|
|
wordX = wordX + 0.25
|
|
----
|
|
|
|
Jetzt braucht es vier Frames für eine Spalte, und das Wort kriecht. `wordX` ist meistens eine Zahl
|
|
wie `12.75`, und das ist überhaupt keine Spalte; Singe löst das, indem es beim Zeichnen alles nach
|
|
dem Punkt wegwirft: `12.75` wird in Spalte zwölf gezeichnet. Genau weil der Bruchteil in der
|
|
Variablen bleibt, kannst du dich langsamer als eine Spalte pro Frame bewegen. Die Zahl ist feiner
|
|
als der Bildschirm.
|
|
|
|
[#_where_is_the_edge]
|
|
=== Wo ist der Rand?
|
|
|
|
Das Wort haut immer noch ab. Um es umkehren zu lassen, musst du wissen, wo der rechte Rand ist, und
|
|
das könntest du durch Zählen herausfinden: Ein Spiel ohne Video bekommt standardmäßig eine Welt von
|
|
720 mal 480, das Overlay, auf das du zeichnest, ist halb so groß, 360 mal 240, und die Buchstaben
|
|
der eingebauten Konsolenschrift sind sechs Pixel breit, also gibt es sechzig Spalten.
|
|
|
|
Mach das nicht. Frag die Engine.
|
|
|
|
[source,lua]
|
|
----
|
|
lastColumn = overlayGetWidth() // CELL_WIDTH - string.len(word)
|
|
----
|
|
|
|
`overlayGetWidth` und `overlayGetHeight` sind zwei weitere Funktionen, die Singe bereitstellt, wie
|
|
`overlayPrint`. Sie bekommen nichts und geben eine Zahl zurück: wie breit und wie hoch die
|
|
Zeichenfläche ist. In Lektion eins hast du gesehen, wie `return` einen Wert zurückgibt, und das
|
|
hier ist das andere Ende davon -- eine Funktion, die du aufrufst, kann dir etwas geben, und hier
|
|
rechnest du mit dem, was sie dir gegeben hat.
|
|
|
|
Die Zahlen, die sie dir geben, sind in Pixeln, und `overlayPrint` zählt in Zeichenzellen, also
|
|
musst du teilen. `overlayGetFontWidth` sagt dir, wie viele Pixel breit eine Zelle ist, und
|
|
`overlayGetFontHeight`, wie hoch, sodass du die Antwort nie selbst kennen musst. Die zwei
|
|
Schrägstriche heißen *teilen und den Bruchteil wegwerfen*, denn ein Drittel einer Zeichenzelle ist
|
|
kein Ort, an dem du etwas ausgeben kannst. Der gewöhnliche einfache Schrägstrich würde dir einen
|
|
Bruch geben, und ein Bruchteil einer Zelle ist auch kein Ort.
|
|
|
|
`string.len` zählt die Zeichen in einem String, und es steht da, damit das Wort umkehrt, wenn sein
|
|
*letzter* Buchstabe den Rand erreicht, und nicht sein erster. Probier es ohne, und du siehst sechs
|
|
Zeichen ins Nichts gleiten.
|
|
|
|
Die Großbuchstaben in `CELL_WIDTH`, das festhält, was dir `overlayGetFontWidth` gibt, sind eine
|
|
Gewohnheit, keine Regel. Lua ist das egal, aber mit einem Namen in Großbuchstaben sagen Programmierer „das ist eine Zahl, die ich einmal oben festlege
|
|
und nie wieder anfasse“, und das ist besser als eine nackte `6` mitten in einer Datei, die einen
|
|
Monat später keiner erklären kann.
|
|
|
|
[#_turn_it_round]
|
|
=== Lass es umkehren
|
|
|
|
In die andere Richtung zu laufen ist überhaupt keine Arbeit: Statt in jedem Frame `0.25`
|
|
dazuzuzählen, zähl `-0.25` dazu. Bewahr den Betrag also in einer eigenen Variablen auf und dreh
|
|
sein Vorzeichen um, wenn das Wort an einem Rand ankommt.
|
|
|
|
[source,lua]
|
|
----
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
speedX = -speedX
|
|
end
|
|
----
|
|
|
|
Das ist eine Entscheidung, und um Entscheidungen geht es in der ganzen Lektion drei, also nimm es
|
|
für eine Lektion einfach hin. Lies es laut: *wenn* `wordX` über `lastColumn` hinaus ist, *dann*
|
|
mach die zwei eingerückten Zeilen; sonst überspring sie. Das `end` schließt es ab, so wie `end`
|
|
eine Funktion abschließt.
|
|
|
|
Die erste Zeile heftet das Wort genau an den Rand, damit ein schnelles Wort nicht darüber
|
|
hinaussegeln kann. Die zweite ist das Umdrehen. `-speedX` ist minus das, was in `speedX` steht;
|
|
stand dort `0.25`, steht jetzt `-0.25` darin, und wenn du in jedem Frame `-0.25` dazuzählst,
|
|
läuft das Wort den Weg zurück, den es gekommen ist. Erreicht es den linken Rand, dreht das
|
|
passende `if` es wieder um, und es wird wieder zu `0.25`.
|
|
|
|
Noch zwei davon für den oberen und den unteren Rand, und das Wort springt im ganzen Fenster herum.
|
|
|
|
[#_the_whole_thing]
|
|
=== Das Ganze
|
|
|
|
[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` hält das Wort aus den obersten zwei Zeilen heraus, damit es nie über den Frame-Zähler
|
|
kritzelt. In `lastRow` steht ein `- 1` statt eines `string.len`, weil ein Wort viele Zeichen breit,
|
|
aber immer nur eine Zeile hoch ist.
|
|
|
|
Beachte, dass die ganze obere Hälfte der Datei einmal läuft und die ganze untere Hälfte sechzigmal
|
|
pro Sekunde, und dass du an der Einrückung erkennst, welche welche ist. Dafür waren die Leerzeichen
|
|
da.
|
|
|
|
[#_try_it_2]
|
|
=== Probier es aus
|
|
|
|
. *Ändere die Geschwindigkeiten.* Probier `speedX = 1` und `speedY = 1`. Dann probier `0.05` für
|
|
beide. Dann mach eine viel größer als die andere.
|
|
. *Ändere das Wort.* Schreib ein viel längeres Wort zwischen die Anführungszeichen -- lang genug,
|
|
um ein Drittel des Fensters zu füllen -- und sieh zu, wie das Abprallen rechts immer noch an der
|
|
richtigen Stelle passiert. Das ist `string.len`, das seine Arbeit tut.
|
|
. *Nimm das Löschen raus.* Lösch die Zeile `overlayClear()`. Lektion eins hat versprochen, dass das
|
|
eines Tages wichtig wird, und heute ist der Tag: Das Wort schmiert eine Spur quer durchs Fenster
|
|
und radiert nichts davon weg, weil Zeichnen schon immer über das gezeichnet hat, was da war. Setz
|
|
sie wieder ein.
|
|
. *Halt die Seitwärtsbewegung an.* Setz `speedX = 0` und starte es. Finde heraus, warum das Wort
|
|
jetzt nie Spalte null verlässt und warum das `if` für den linken Rand nie anspringt.
|
|
. *Mach den Frame-Zähler absichtlich kaputt.* Schieb die Zeile `frames = 0` zurück in die
|
|
Funktion, über `frames = frames + 1`. Sieh zu, wie der Zähler bei eins hängen bleibt, während
|
|
sich das Wort tadellos weiterbewegt. Dann finde heraus, warum das Wort nichts davon merkt.
|
|
|
|
[#_break_it_on_purpose_2]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Ersetze die zwei Punkte in der Zeile mit dem Frame-Zähler durch ein Pluszeichen, sodass sie so
|
|
lautet:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " + frames)
|
|
----
|
|
|
|
Speichere. Das Fenster geht zu, Singe hält an, und im Terminal steht:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': numbers/numbers.singe:42: attempt to add a 'string' with a 'number'
|
|
----
|
|
|
|
gefolgt von mehreren Zeilen, die mit `stack traceback:` anfangen und die du vorerst ignorieren
|
|
kannst.
|
|
|
|
Lies es so, wie Lektion eins es dir beigebracht hat: die Datei, die Zeile, dann die Beschwerde.
|
|
Zeile 42 ist die Zeile, die du geändert hast. Die Beschwerde sagt, Lua sollte einen String zu einer
|
|
Zahl *addieren*, und Addieren ist etwas für Zahlen. `+` und `..` sehen auf dem Papier ähnlich aus
|
|
und erledigen völlig verschiedene Aufgaben: Plus ist Rechnen, `..` ist Kleber.
|
|
|
|
Diesen Fehler lohnt es sich kennenzulernen, wegen der Stelle, an der er auftaucht. Der Fehler aus
|
|
Lektion eins hat Singe angehalten, bevor dein Spiel überhaupt anfing, weil Lua die Datei gar
|
|
nicht lesen konnte. Dieser hier steckt in einer völlig lesbaren Zeile, die erst schiefgeht, wenn sie
|
|
läuft; also ging das Fenster auf, das Spiel lief, und es starb im ersten Frame. Beide Arten nennen
|
|
dir die Datei und die Zeile. Fang so oder so dort an.
|
|
|
|
[#_what_you_learned_2]
|
|
=== Was du gelernt hast
|
|
|
|
* Eine Variable ist ein Name mit einem Wert dahinter, und der Wert kann sich ändern.
|
|
* `=` heißt „leg die rechte Seite in den Namen links“, nicht „ist gleich“.
|
|
* `frames = frames + 1` rechnet zuerst die rechte Seite aus und speichert sie dann.
|
|
* `+`, `-`, `*` und `/` rechnen, wie du es erwartest; `//` teilt und wirft den Bruchteil weg.
|
|
* `..` hängt Text aneinander und macht dabei aus einer Zahl Text.
|
|
* Eine Zeile in `onOverlayUpdate` läuft in jedem Frame. Eine Zeile außerhalb jeder Funktion läuft
|
|
einmal, bevor das Spiel anfängt.
|
|
* Alles, was sich von einem Frame zum nächsten etwas merken muss, muss draußen wohnen.
|
|
* `overlayGetWidth` und `overlayGetHeight` sagen dir die Größe der Zeichenfläche, damit du sie nie
|
|
raten musst.
|
|
* `overlayClear` wird wichtig, sobald sich irgendetwas bewegt.
|
|
|
|
[#_next_time_2]
|
|
=== Beim nächsten Mal
|
|
|
|
Das Wort bewegt sich, aber es bewegt sich von selbst, und nichts, was du tust, ändert etwas daran.
|
|
In Lektion drei nimmst du dir die vier `if`-Blöcke vor, die du auf Treu und Glauben benutzt hast,
|
|
lernst, was sie wirklich sagen, und richtest sie auf die Pfeiltasten statt auf die Ränder des
|
|
Bildschirms. Das Kästchen, das du am Ende durchs Fenster steuerst, ist die Spielfigur des Spiels,
|
|
das du in Lektion sieben baust.
|