singe/docs/de/lessons/03-decisions.adoc

503 lines
20 KiB
Text

[#_lesson_3_making_decisions]
== Lektion 3: Entscheidungen treffen
image::learn/03-decisions.png[Die fertige Lektion, 480]
Alles, was du bisher geschrieben hast, tut in jedem Frame dasselbe. Das Wort aus Lektion zwei hat
sich bewegt, ob du wolltest oder nicht, und umgekehrt ist es nur wegen vier Blöcken Code, die du
auf Treu und Glauben hinnehmen solltest.
In dieser Lektion geht es um diese vier Blöcke, und zwar richtig. Am Ende stellt dein Programm
Fragen, handelt nach den Antworten, und -- weil die interessanteste Frage, die ein Spiel stellen
kann, „Was macht der Spieler?“ lautet -- du steuerst ein Kästchen mit den Pfeiltasten durchs
Fenster. Heb dieses Kästchen gut auf. Es ist die Spielfigur des Spiels, das du in Lektion sieben
baust.
[#_start_a_new_folder_2]
=== Leg einen neuen Ordner an
Ein Ordner namens `decisions`, darin eine Datei namens `decisions.singe`, und in einem Terminal
läuft `Singe -R decisions`. Wie beim letzten Mal.
[#_what_an_if_really_says]
=== Was ein `if` wirklich sagt
Hier ist der Block aus Lektion zwei für sich allein.
[source,lua]
----
if wordX > lastColumn then
wordX = lastColumn
end
----
Vier Teile. Das Wort `if`. Eine Frage. Das Wort `then`. Und ein Rumpf aus Zeilen, die nur laufen,
wenn die Antwort auf die Frage Ja ist, abgeschlossen mit `end`.
Die Frage ist `wordX > lastColumn`, und die eigentliche Arbeit macht das `>`. Es vergleicht die
beiden Zahlen links und rechts davon und liefert eine Antwort, die entweder *wahr* oder *falsch*
ist. Diese beiden Wörter sind kein Text und keine Zahlen; sie sind eigene Werte, *Booleans*
genannt, und ein Boolean ist das Einzige, wofür sich ein `if` interessiert.
Die Einrückung ist dieselbe Idee wie in Lektion eins: Die Zeilen darin sind eingerückt, damit du
auf einen Blick siehst, was zum `if` gehört, und das `end` steht genau unter dem `if`, das den
Block geöffnet hat. Lua würde es auch ohne die Leerzeichen ausführen. Du könntest es dann nicht
lesen.
Es gibt sechs solche Vergleiche, und du wirst alle benutzen.
[cols="1,4"]
|===
| `a < b` | a ist kleiner als b
| `a > b` | a ist größer als b
| `a <= b` | a ist kleiner oder gleich b
| `a >= b` | a ist größer oder gleich b
| `a == b` | a ist dasselbe wie b
| `a ~= b` | a ist nicht dasselbe wie b
|===
Bei den letzten beiden musst du aufpassen, aus verschiedenen Gründen.
`~=` heißt „ist nicht gleich“. Die meisten Sprachen schreiben das `!=`, und wenn du schon einmal
ein Programm gesehen hast, tippen deine Finger `!=`, und Lua lehnt es ab. Die Schlangenlinie liegt
auf deiner Tastatur neben der `1` oder unter dem `#`, je nachdem, wo du wohnst.
`==` sind zwei Gleichheitszeichen, und das ist kein Tippfehler für eins. Das ist einer der
häufigsten Fehler beim Programmieren, und es lohnt sich, kurz dabei stehen zu bleiben.
In Lektion zwei hast du gelernt, dass ein Gleichheitszeichen *leg die rechte Seite in den Namen
links* bedeutet. Das ist etwas, das du tust. Es ändert etwas.
Zwei Gleichheitszeichen bedeuten *sind die beiden gleich?* Das ist eine Frage, die du stellst. Sie
ändert nichts, und sie gibt wahr oder falsch zurück.
[source,lua]
----
lives = 3
if lives == 3 then
----
Die erste Zeile setzt `lives` auf drei. Die zweite fragt, ob es drei ist. Verschiedene Aufgaben,
verschiedene Symbole, ein Zeichen Unterschied. Lua erwischt dich, wenn du sie verwechselst, und den
Fehler, den es dann meldet, lernst du am Ende dieser Lektion kennen.
[#_when_the_answer_is_no]
=== Wenn die Antwort Nein ist
Ein `if` allein tut überhaupt nichts, wenn die Antwort falsch ist. Sehr oft willst du, dass
stattdessen das andere passiert, und dafür gibt es `else`.
[source,lua]
----
if lives > 0 then
message = "Ready"
else
message = "Game Over"
end
----
Genau einer der beiden Rümpfe läuft, nie beide, nie keiner, also steht in `message` am Ende immer
einer der beiden Strings und nie gar nichts.
Wenn es mehr als zwei Möglichkeiten gibt, hängt `elseif` sie aneinander.
[source,lua]
----
if score >= 10000 then
rank = "ace"
elseif score >= 5000 then
rank = "pilot"
elseif score >= 1000 then
rank = "rookie"
else
rank = "passenger"
end
----
Lua probiert die Fragen von oben und hört bei der ersten auf, die wahr ist. Eine Punktzahl von 6000
ist nicht größer als 10000, also probiert es die nächste, und die ist wahr, also wird `rank` zu
`pilot`, und die anderen beiden Fragen werden nie gestellt. Dieser letzte Punkt ist wichtig: Das
`elseif` für 1000 ist bei einer Punktzahl von 6000 auch wahr, aber es wird nie erreicht, und darum
geht die Reihenfolge vom Größten zum Kleinsten.
Die ganze Kette braucht genau ein `end`, ganz egal, wie viele `elseif`-Teile darin stecken. Es ist
eine Entscheidung, nicht vier.
[#_true_false_and_nothing_at_all]
=== Wahr, falsch und gar nichts
Du musst nicht zwei Dinge vergleichen, um einen Boolean zu bekommen. Du kannst einen aufbewahren.
[source,lua]
----
paused = false
----
Das ist eine Variable wie jede andere, und der Wert darin ist der Boolean `false` selbst, ohne
Anführungszeichen geschrieben, weil es nicht das Wort „false“ ist, sondern die Idee. Dasselbe gilt
für `true`. Eine Variable, die einen von beiden enthält, kann direkt in ein `if`, ganz ohne
Vergleich:
[source,lua]
----
if paused then
----
Lies es als „if paused“. Das ist besseres Englisch als `if paused == true`, das dasselbe auf dem
Umweg tut, und erfahrene Programmierer schauen dich seltsam an, wenn du das schreibst.
Drei Wörter verbinden diese Antworten und drehen sie um.
[source,lua]
----
if alive and not shielded then
if coins > 0 or freePlay then
----
`and` ist wahr, wenn beide Seiten wahr sind. `or` ist wahr, wenn eine der beiden Seiten wahr ist,
oder beide. `not` macht aus wahr falsch und aus falsch wahr. Sie lesen sich wie Englisch, und,
ungewöhnlich fürs Programmieren, sie bedeuten auch, was sie im Englischen bedeuten.
Es gibt noch einen Wert, den du kennen solltest, und er ist der, der die verwirrendsten Fehler
deines ersten Jahres erzeugt.
[source,lua]
----
overlayPrint(0, 0, "lives " .. tostring(lives))
----
Wenn du nie etwas in `lives` gelegt hast, gibt das `lives nil` aus. Lua beschwert sich nicht über
einen Namen, den es nie gesehen hat; es gibt dir `nil`, und das ist sein Wort für *hier ist
nichts*. Es ist nicht die Null, und es ist kein leerer String. Die Null ist eine Zahl, die du
speichern wolltest. `nil` ist das Fehlen eines Werts.
Du bist `nil` schon zweimal begegnet, ohne dass es dir jemand gesagt hat. `frames = frames + 1`
ohne die Zeile `frames = 0` darüber schlägt fehl, weil du Lua gebeten hast, eins zu nichts zu
addieren. Und ein Callback, den du falsch schreibst, ist ebenfalls `nil`, und darum bringt dir ein
falsch geschriebenes `onOverlayUpdate` ein schwarzes Fenster und überhaupt keinen Fehler: Singe
sucht nach dem Namen, findet dort nichts und macht still mit seinem Tag weiter.
`tostring` steht in dieser Zeile, weil `..` einen Boolean oder ein `nil` an nichts anhängt. Zahlen
wandelt es für dich um; diese nicht. Das war das Versprechen am Ende von Lektion zwei, und jetzt
hast du eine Verwendung dafür.
Zum Schluss die Regel, die den ganzen Abschnitt zusammenhält, denn `if` nimmt alles, was du ihm
gibst, nicht nur wahr und falsch:
[quote]
In Lua ist alles wahr außer `false` und `nil`.
Null ist wahr. Ein leerer String ist wahr. Nur diese beiden sind es nicht, und wenn du das einmal
weißt, liest sich `if movingLeft then` tadellos, ob in `movingLeft` nun `true` oder
`false` steht oder ob es nie gesetzt wurde.
[#_reading_the_controls]
=== Die Steuerung abfragen
Zeit, den Spieler etwas zu fragen. Dafür braucht es einen neuen Callback.
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_RIGHT then
boxX = boxX + 10
end
end
----
`onInputPressed` ist der zweite Callback, den du geschrieben hast, und er funktioniert genau so, wie
Lektion eins es beschrieben hat: Du schreibst ihn, Singe ruft ihn auf. `onOverlayUpdate` wird in
einem festen Takt aufgerufen. Dieser hier wird aufgerufen, wenn etwas passiert -- wenn der Spieler
irgendetwas drückt, das die Engine erkennt.
Der Unterschied zu `onOverlayUpdate` ist das Wort in den Klammern. Singe muss dir sagen, *welches*
Steuerelement gedrückt wurde, also übergibt es der Funktion einen Wert, und `what` ist der Name,
unter dem du ihn entgegennehmen willst. In Lektion eins hießen die Dinge, die du einer Funktion
übergibst, Argumente; von innen gesehen ist der Name, der eines auffängt, ein *Parameter*. Der Name
gehört dir: Nenn ihn `what`, nenn ihn `button`, nenn ihn `k`. Singe füllt ihn so oder so.
`SWITCH_RIGHT` ist ein Name, den die Engine schon einem Wert gegeben hat, wie `OVERLAY_UPDATED` in
Lektion eins. Du musst nichts laden, um ihn zu benutzen; Singe legt sie alle fest, bevor dein Skript
läuft. Es gibt einen für alles, was ein Automat hat:
`SWITCH_LEFT`, `SWITCH_RIGHT`, `SWITCH_UP`, `SWITCH_DOWN`, `SWITCH_BUTTON1`
bis `SWITCH_BUTTON4`, `SWITCH_START1`, `SWITCH_COIN1` und mehr. Die Tabelle
Engine-Konstanten in der Referenz deckt die ganze Familie ab, die von `SWITCH_UP` bis
`SWITCH_RECORD` reicht. Sie heißen absichtlich nicht „die rechte Pfeiltaste“, denn dasselbe
`SWITCH_RIGHT` kommt an, ob der Spieler eine Pfeiltaste gedrückt hat, einen Joystick oder den
Stick eines Gamepads. Du schreibst das Spiel einmal, und es funktioniert mit allen dreien.
Füg diesen Callback hinzu, gib `boxX` außerhalb der Funktionen einen Startwert und zeichne etwas
bei `boxX`. Drück die Pfeiltaste nach rechts. Das Kästchen springt bei jedem Drücken zehn Pixel
weiter.
[#_it_happened_and_it_is_happening]
=== Es ist passiert, und es passiert gerade
Halt die Pfeiltaste gedrückt. Mehr passiert nicht.
`onInputPressed` wird einmal aufgerufen, beim Hinunterdrücken, und erst wieder, wenn du loslässt
und noch einmal drückst. Es gibt keine Tastenwiederholung. Für manche Dinge ist das genau richtig
-- einen Schuss abfeuern, eine Münze einwerfen, einen Menüpunkt wählen -- und fürs Laufen genau
falsch, denn das muss so lange weitergehen, wie die Taste gehalten wird.
Das sind wirklich zwei verschiedene Fragen. *Es ist passiert* ist ein Ereignis, und davon hörst du
einmal. *Es passiert gerade* ist ein Zustand, und der dauert an. Singe erzählt dir von Ereignissen,
und aus Ereignissen machst du einen Zustand, indem du sie dir selbst merkst.
Zwei Callbacks, eine Variable dazwischen.
[source,lua]
----
movingRight = false
function onInputPressed(what)
if what == SWITCH_RIGHT then
movingRight = true
end
end
function onInputReleased(what)
if what == SWITCH_RIGHT then
movingRight = false
end
end
----
`onInputReleased` ist der dritte Callback, und er ist das Spiegelbild des zweiten: Singe ruft ihn
auf dem Weg zurück nach oben auf, mit demselben `SWITCH_`-Wert. Zusammen sorgen die beiden dafür,
dass `movingRight` die Wahrheit darüber sagt, was der Finger des Spielers gerade tut.
Bewegt hat sich noch nichts, und genau darum geht es. Das Drücken und das Bewegen sind jetzt zwei
getrennte Aufgaben. Das Bewegen kommt dorthin, wo alle Arbeit hinkommt, die in jedem Frame
passiert.
[source,lua]
----
function onOverlayUpdate()
if movingRight then
boxX = boxX + BOX_SPEED
end
...
----
Das Kästchen gleitet jetzt gleichmäßig, solange die Taste unten ist, mit einer Geschwindigkeit, die
du bestimmst, und das Rechnen ist wieder in der einen Funktion, die sechzigmal pro Sekunde läuft,
wo es hingehört. Diese Form -- Ereignisse setzen Flags, der Frame handelt nach den Flags -- ist die
Art, wie fast jedes Spiel seine Steuerung behandelt, und du wirst sie in jeder Lektion wieder
benutzen, in der es eine Spielfigur gibt.
[#_drawing_a_box]
=== Ein Kästchen zeichnen
`overlayBox` bekommt zwei Ecken: links und oben am Rechteck, dann rechts und unten.
[source,lua]
----
colorForeground(255, 220, 0)
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
----
Es zeichnet einen *Umriss*, vier Linien, ein Pixel dick, und kein gefülltes Rechteck. Unter den
Zeichenaufrufen fürs Overlay gibt es überhaupt kein gefülltes Rechteck. Wenn du später einen
massiven Block willst, gibt es Wege dahin, und Lektion neun zeigt den üblichen.
Seine Zahlen sind Pixel, keine Zeichenzellen. Darauf fallen Leute herein, denn `overlayPrint` hat
in Lektion zwei in Zellen von sechs mal dreizehn Pixeln gezählt, und alles andere in Singe zählt in
Pixeln. `overlayPrint` ist der Sonderling; die übrigen Zeichenaufrufe -- `overlayBox`,
`overlayLine`, `overlayCircle`, `overlayEllipse` und `overlayPlot` -- arbeiten alle in
denselben Pixeln, die `overlayGetWidth` meldet.
`colorForeground` legt die Farbe fest, als drei Zahlen von 0 bis 255: wie viel Rot, wie viel Grün,
wie viel Blau. Alle drei auf 255 ist Weiß, alle auf 0 ist Schwarz, und `(255, 220, 0)` ist ein
warmes Gelb. Beachte, dass es kein Argument von `overlayBox` ist. Es ist eine Einstellung: Du
änderst sie, und alles, was du danach zeichnest, kommt in der neuen Farbe heraus, bis du sie wieder
änderst. `overlayPrint` ist wieder einmal die Ausnahme -- die Konsolenschrift bringt ihre eigenen
Farben fest eingebacken mit und ignoriert dich.
[#_the_whole_thing_2]
=== Das Ganze
[source,lua]
----
BOX_SIZE = 20
BOX_SPEED = 2
width = overlayGetWidth()
height = overlayGetHeight()
maxX = width - BOX_SIZE - 1
maxY = height - BOX_SIZE - 1
boxX = width // 2
boxY = height // 2
movingLeft = false
movingRight = false
movingUp = false
movingDown = false
function onInputPressed(what)
if what == SWITCH_LEFT then
movingLeft = true
elseif what == SWITCH_RIGHT then
movingRight = true
elseif what == SWITCH_UP then
movingUp = true
elseif what == SWITCH_DOWN then
movingDown = true
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
movingLeft = false
elseif what == SWITCH_RIGHT then
movingRight = false
elseif what == SWITCH_UP then
movingUp = false
elseif what == SWITCH_DOWN then
movingDown = false
end
end
function onOverlayUpdate()
if movingLeft and not movingRight then
boxX = boxX - BOX_SPEED
elseif movingRight and not movingLeft then
boxX = boxX + BOX_SPEED
end
if movingUp and not movingDown then
boxY = boxY - BOX_SPEED
elseif movingDown and not movingUp then
boxY = boxY + BOX_SPEED
end
if boxX < 0 then
boxX = 0
elseif boxX > maxX then
boxX = maxX
end
if boxY < 0 then
boxY = 0
elseif boxY > maxY then
boxY = maxY
end
overlayClear()
if movingLeft or movingRight or movingUp or movingDown then
colorForeground(255, 220, 0)
else
colorForeground(80, 160, 255)
end
overlayBox(boxX, boxY, boxX + BOX_SIZE, boxY + BOX_SIZE)
overlayPrint(0, 0, "x " .. boxX .. " y " .. boxY)
return OVERLAY_UPDATED
end
----
Vier Dinge darin sind einen zweiten Blick wert.
`movingLeft and not movingRight` steht da, weil ein Spieler beide Pfeiltasten gleichzeitig halten
kann. Ohne das `not` würden die beiden gegeneinander kämpfen, und das Kästchen würde zittern oder
wegdriften; mit ihm gewinnt keine, wenn beide gehalten werden, und das Kästchen steht still, und
genau das erwartet ein Spieler.
`maxX` wird einmal oben ausgerechnet, mit `overlayGetWidth`, so wie in Lektion zwei, und das `- 1`
ist da, weil das letzte Pixel der Fläche um eins kleiner ist als ihre Breite. Nimm das `- 1`
heraus, und die rechte Kante des Kästchens fällt von der Fläche und verschwindet, weil Singe alles,
was du außerhalb des Overlays zeichnest, wortlos wegwirft.
Die Begrenzungsblöcke benutzen `elseif` statt zweier getrennter `if`-Blöcke. Das Kästchen kann
nicht gleichzeitig links und rechts über den Rand hinaus sein, also ist es verschwendete Arbeit,
die zweite Frage zu stellen, wenn die erste wahr war.
Und die Farbentscheidung ist ein `if`, dessen einzige Aufgabe es ist, einen Wert auszuwählen. Gelb,
solange der Spieler etwas tut, blau, solange nicht, und so siehst du gratis, dass deine
Eingabeverarbeitung wirklich funktioniert.
[#_try_it_3]
=== Probier es aus
. *Ändere die Geschwindigkeit.* `BOX_SPEED = 1` und `BOX_SPEED = 8`. Sieh dir bei acht an, was die
Begrenzung macht, wenn du das Kästchen in eine Ecke schiebst.
. *Zeig deine Flags.* Füg `onOverlayUpdate` eine Zeile hinzu:
`overlayPrint(0, 1, "left " .. tostring(movingLeft))`. Halte die linke Pfeiltaste, lass sie
los und sieh zu, wie der Wert umspringt. Dann nimm das `tostring` weg und lies den Fehler, den
Lua dir gibt.
. *Mach es schneller, wenn ein Knopf gedrückt ist.* Füg ein Flag `running` hinzu, das in den
beiden Eingabe-Callbacks von `SWITCH_BUTTON1` gesteuert wird, und benutze ein `if` in
`onOverlayUpdate`, um dich um das Doppelte von `BOX_SPEED` zu bewegen, solange es wahr ist.
. *Markier die Mitte.* Zeichne ein zweites, kleines Kästchen in die Mitte des Fensters, das die
Farbe wechselt, wenn das Kästchen des Spielers es überlappt. Du brauchst `and`, um vier
Vergleiche zu verbinden, und es ist schwieriger, als es klingt. Es ein bisschen falsch
hinzubekommen ist normal; Lektion elf macht es richtig.
. *Nimm das Loslassen raus.* Lösch die ganze Funktion `onInputReleased` und starte es. Das
Kästchen macht einen Schritt und hält dann nie wieder an. Finde heraus, welche Zeile jetzt nie
erreicht wird.
[#_break_it_on_purpose_3]
=== Mach es absichtlich kaputt
Such den Begrenzungsblock und mach aus dem `<` in seinem ersten Vergleich ein einzelnes
Gleichheitszeichen, sodass er so lautet:
[source,lua]
----
if boxX = 0 then
----
Speichere. Das Fenster bleibt leer, und im Terminal steht:
----
Error running script: decisions/decisions.singe:57: 'then' expected near '='
----
Das ist der Fehler, vor dem der ganze Abschnitt über `==` gewarnt hat, und die Meldung ist ein
gutes Beispiel für einen Fehler, der völlig zutreffend und völlig nutzlos ist, wenn du ihn wörtlich
nimmst. Lua will nicht, dass du das Wort `then` hinzufügst. Lua ist bis `if boxX` gekommen, hat
als Nächstes einen Vergleich oder ein `then` erwartet und `=` gefunden, und das kann keins von
beiden sein.
Immer wenn du `'then' expected` nach einem `if` siehst, such ein einfaches `=`, das ein doppeltes
sein sollte. Fast immer ist es das.
Achte darauf, wo dieser hier aufgetaucht ist: Das Spiel ist nie gestartet, genau wie beim
unvollständigen String in Lektion eins, weil Lua die Datei nicht lesen konnte. Der Fehler mit dem
Frame-Zähler in Lektion zwei passierte während des Laufens. Fehler in der Form deines Codes halten
dich auf, bevor das Spiel anfängt. Fehler in dem, was dein Code tut, warten, bis er läuft.
[#_what_you_learned_3]
=== Was du gelernt hast
* `if question then ... end` führt einen Rumpf nur aus, wenn die Antwort wahr ist.
* `else` liefert den anderen Fall, und `elseif` hängt weitere an; die erste wahre Frage gewinnt, und
die übrigen werden nie gestellt.
* Die Vergleiche sind `<`, `>`, `<=`, `>=`, `==` und `~=`.
* `=` legt einen Wert in einen Namen. `==` fragt, ob zwei Dinge gleich sind.
* `true` und `false` sind Werte, und du kannst sie in Variablen aufbewahren.
* `and`, `or` und `not` verbinden diese Antworten und drehen sie um.
* `nil` bekommst du von einem Namen, den du nie gesetzt hast, und es ist nicht die Null.
* Alles ist wahr außer `false` und `nil`.
* `onInputPressed` und `onInputReleased` sind Callbacks, die Singe mit einem `SWITCH_`-Wert
aufruft, wenn der Spieler ein Steuerelement drückt oder loslässt.
* Ein Drücken ist ein Ereignis und passiert einmal; halte den Zustand in einer Variablen fest und
handle in jedem Frame danach.
* `overlayBox` zeichnet einen Umriss in Pixeln, und `colorForeground` ist eine Einstellung, kein
Argument.
[#_next_time_3]
=== Beim nächsten Mal
Ein Kästchen ist leicht. Hundert Sterne sind es nicht, zumindest nicht so, wie du bisher
geschrieben hast, denn hundert Zeilen `overlayPlot` abzutippen ist niemandes Vorstellung von einem
schönen Abend. In Lektion vier geht es um die Schleife: dem Computer zu sagen, dass er dasselbe
viele Male tun soll, jedes Mal mit etwas anderem, und erst das macht einen Bildschirm voll mit
irgendwas möglich.