686 lines
25 KiB
Text
686 lines
25 KiB
Text
[#_lesson_5_your_own_functions]
|
|
== Lektion 5: Deine eigenen Funktionen
|
|
|
|
image::learn/05-functions.png[Die fertige Lektion, 480]
|
|
|
|
Lektion vier hat mit einer Warnung über dein eigenes Programm geendet: `onOverlayUpdate` rechnet
|
|
aus, wohin hundertzwanzig Sterne kommen, entscheidet, wie hell jeder ist, zeichnet sie und zählt
|
|
die Drift, und das ist eine Funktion, die vier Aufgaben erledigt. Gleich soll sie eine fünfte
|
|
übernehmen, denn ein Sternenfeld, vor dem nichts ist, ist kein Spiel.
|
|
|
|
In dieser Lektion nimmst du sie auseinander. Du schreibst eigene Funktionen -- Funktionen, von
|
|
denen Singe nie gehört hat und die es nie aufrufen wird -- und gibst die Arbeit an sie ab. Am Ende
|
|
ist das ganze Sternenfeld eine Zeile, in der `drawStars()` steht, unten auf dem Bildschirm ist
|
|
wieder ein Schiff, und das Programm ist leichter zu lesen als damals, als es weniger getan hat.
|
|
|
|
[#_start_a_new_folder_4]
|
|
=== Leg einen neuen Ordner an
|
|
|
|
Ein Ordner namens `functions`, darin eine Datei namens `functions.singe`, und in einem Terminal
|
|
läuft `Singe -R functions`.
|
|
|
|
Kopier das fertige Programm aus Lektion vier hinein. Hier ist es, unverändert, und es lohnt sich,
|
|
es einmal laufen zu lassen, bevor du irgendetwas anfasst:
|
|
|
|
[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
|
|
----
|
|
|
|
Drei Ebenen von Sternen, die mit drei Geschwindigkeiten das Fenster hinuntertreiben. Nichts in der
|
|
ersten Hälfte dieser Lektion ändert, was du siehst. Alles darin ändert, wie sich das Programm
|
|
liest.
|
|
|
|
[#_one_job_one_name]
|
|
=== Eine Aufgabe, ein Name
|
|
|
|
Fang mit den zwei Zeilen an, die einen Stern auf den Bildschirm bringen. Füg über
|
|
`onOverlayUpdate` das hier ein:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStar(x, y, shade)
|
|
colorForeground(shade, shade, shade)
|
|
overlayPlot(x, y)
|
|
end
|
|
----
|
|
|
|
Dann lösch in der inneren Schleife diese zwei Zeilen und ruf stattdessen die Funktion auf:
|
|
|
|
[source,lua]
|
|
----
|
|
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
|
|
drawStar(starX, starY, shade)
|
|
end
|
|
----
|
|
|
|
Speichere. Das Bild ist dasselbe.
|
|
|
|
Du hast schon früher Funktionen geschrieben -- `onOverlayUpdate` ist eine, `onInputPressed` und
|
|
`onInputReleased` aus Lektion drei ebenso. Aber das sind Callbacks. Du schreibst sie, und Singe
|
|
ruft sie auf. `drawStar` ist anders. Singe hat nie von ihr gehört. Sie läuft, weil *du* ihren
|
|
Namen und Klammern geschrieben hast, genauso wie du `overlayPlot` aufrufst, und nichts auf der Welt
|
|
führt sie aus, wenn du es nicht tust.
|
|
|
|
Das ist der ganze Trick. Wenn du ab jetzt denkst „und jetzt zeichne einen Stern“, kannst du
|
|
„zeichne einen Stern“ hinschreiben und es so meinen.
|
|
|
|
[#_a_parameter_is_a_name_not_a_thing]
|
|
=== Ein Parameter ist ein Name, kein Ding
|
|
|
|
Sieh dir die erste Zeile von `drawStar` noch einmal an.
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStar(x, y, shade)
|
|
----
|
|
|
|
Die drei Namen in den Klammern sind *Parameter*. Ein Parameter ist ein Name, den die Funktion für
|
|
das benutzt, was der Aufrufer ihr übergeben hat. Wenn du `drawStar(starX, starY, shade)` schreibst,
|
|
setzt Lua `x` auf das, was in `starX` steht, `y` auf das, was in `starY` steht, `shade` auf das,
|
|
was in `shade` steht, und führt dann die Funktion aus.
|
|
|
|
Die Namen haben nichts miteinander zu tun. Die Zahl des Aufrufers heißt da draußen `starX` und hier
|
|
drinnen `x`, und es würde genauso funktionieren, wenn du sie `across` oder `banana` nennen würdest.
|
|
In der Funktion gibt es nur die Namen in den Klammern, und sie enthalten Kopien von dem, was
|
|
hereingekommen ist.
|
|
|
|
Hier ergeben die leeren Klammern aus Lektion eins endlich einen Sinn.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
----
|
|
|
|
Nichts in den Klammern heißt keine Parameter: Die Engine hat dieser Funktion nichts zu sagen, also
|
|
gibt es nichts zu benennen. Vergleich das mit dem Callback, den du in Lektion drei geschrieben
|
|
hast.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
----
|
|
|
|
Ein Parameter namens `what`. Diesem hier hat die Engine etwas zu sagen -- welches Steuerelement
|
|
gedrückt wurde -- also gibt es einen Namen, der es auffängt. Du benutzt seit Lektion drei einen
|
|
Parameter.
|
|
|
|
Ein Parameter ist wirklich die eigene Kopie der Funktion, und es lohnt sich, dir das einmal selbst
|
|
zu beweisen. Tipp das ein, ruf es aus `onOverlayUpdate` auf und nimm es danach wieder heraus:
|
|
|
|
[source,lua]
|
|
----
|
|
function half(number)
|
|
number = number // 2
|
|
return number
|
|
end
|
|
----
|
|
|
|
Ruf sie als `half(drift)` auf. Die Funktion halbiert ihr `number` und gibt die Antwort zurück, und
|
|
`drift` da draußen in `onOverlayUpdate` zählt weiter, als wäre nichts passiert. Eine Funktion kann
|
|
nicht über einen Parameter hinausgreifen und die Variablen des Aufrufers ändern. Sie bekommt eine
|
|
Kopie, und was sie mit der Kopie macht, ist ihre Sache.
|
|
|
|
[#_handing_an_answer_back]
|
|
=== Eine Antwort zurückgeben
|
|
|
|
`half` hat etwas getan, was die Funktionen, die du bisher geschrieben hast, nicht getan haben: Sie
|
|
hat eine Antwort gegeben. Das ist die letzte Zeile.
|
|
|
|
[source,lua]
|
|
----
|
|
return number
|
|
----
|
|
|
|
`return` gibt einen Wert an den zurück, der die Funktion aufgerufen hat, und der Aufruf wird zu
|
|
diesem Wert. Schreib `smaller = half(drift)`, und Lua führt `half` aus, nimmt, was `half`
|
|
zurückgegeben hat, und legt es in `smaller`. Du hast `return` in Lektion eins kennengelernt, wo
|
|
`onOverlayUpdate` den Wert `OVERLAY_UPDATED` zurückgibt, um Singes Frage zu beantworten, ob sich
|
|
etwas geändert hat. Das ist dasselbe Wort mit derselben Aufgabe, nur dass jetzt du derjenige bist,
|
|
der die Frage stellt.
|
|
|
|
Die Zeile mit der Helligkeit bettelt geradezu darum, eine Funktion mit einer Antwort zu werden. Füg
|
|
diese hier hinzu:
|
|
|
|
[source,lua]
|
|
----
|
|
function shadeFor(layer)
|
|
return math.random(60, 90) + layer * LAYER_BRIGHT
|
|
end
|
|
----
|
|
|
|
Der ganze Rumpf ist ein `return`, und das ist bei einer kleinen Funktion normal. Jetzt kann die
|
|
innere Schleife die Antwort direkt an `drawStar` weitergeben, ohne einen Namen dazwischen:
|
|
|
|
[source,lua]
|
|
----
|
|
drawStar(starX, starY, shadeFor(layer))
|
|
----
|
|
|
|
Ein Aufruf in einem Aufruf. Lua rechnet zuerst den inneren aus, bekommt von `shadeFor` eine Zahl
|
|
zurück und übergibt diese Zahl an `drawStar` als drittes Argument. So etwas hast du schon
|
|
geschrieben, ohne es zu merken: Das `math.floor(drift * layer * LAYER_SPEED)` in Lektion vier
|
|
ist ein Aufruf, dessen Antwort sofort an Ort und Stelle benutzt wird, mitten in einer größeren
|
|
Rechnung.
|
|
|
|
[#_two_answers_at_once]
|
|
=== Zwei Antworten auf einmal
|
|
|
|
Die beiden Zeilen für die Position sind schwieriger, weil es zwei Zahlen sind, die zusammen
|
|
ausgerechnet werden, und du müsstest dir eine aussuchen, die du zurückgibst. In den meisten
|
|
Sprachen kann eine Funktion einen Wert zurückgeben, und zwei herauszubekommen ist lästig. Lua lässt
|
|
dich so viele zurückgeben, wie du willst.
|
|
|
|
[source,lua]
|
|
----
|
|
function starAt(star, layer)
|
|
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
|
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
|
return x, y
|
|
end
|
|
----
|
|
|
|
Du fängst die Antworten auf, indem du links zwei Namen hinschreibst, durch ein Komma getrennt:
|
|
|
|
[source,lua]
|
|
----
|
|
local x, y = starAt(star, layer)
|
|
----
|
|
|
|
Der erste Wert kommt in den ersten Namen, der zweite in den zweiten. Gibst du weniger Namen an, als
|
|
die Funktion zurückgibt, werden die übrigen Werte weggeworfen:
|
|
`local x = starAt(star, layer)` gibt dir die waagerechte Zahl und verliert die senkrechte, ohne
|
|
Beschwerde. Gibst du mehr Namen an, als es Werte gibt, sind die überzähligen `nil`.
|
|
|
|
Ignorier das Wort `local` in diesen beiden Blöcken noch eine Minute. Es bekommt gleich einen
|
|
eigenen Abschnitt, und das ist der wichtige.
|
|
|
|
[#_the_star_field_in_one_line]
|
|
=== Das Sternenfeld in einer Zeile
|
|
|
|
Die innere Schleife besteht jetzt aus drei kurzen Zeilen, und alle drehen sich um eine Ebene von
|
|
Sternen. Das ist eine Aufgabe mit einem Namen.
|
|
|
|
[source,lua]
|
|
----
|
|
function drawLayer(layer)
|
|
for star = 1, STARS_PER_LAYER do
|
|
local x, y = starAt(star, layer)
|
|
drawStar(x, y, shadeFor(layer))
|
|
end
|
|
end
|
|
----
|
|
|
|
Und die äußere Schleife ist auch eine Aufgabe mit einem Namen:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStars()
|
|
for layer = 1, LAYERS do
|
|
drawLayer(layer)
|
|
end
|
|
end
|
|
----
|
|
|
|
Funktionen, die Funktionen aufrufen, die Funktionen aufrufen, drei Stufen tief, und jede ist drei
|
|
oder vier Zeilen lang, die du auf einmal im Kopf behalten kannst. `onOverlayUpdate` kann jetzt
|
|
sagen, was ein Frame *ist*:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
overlayClear()
|
|
drawStars()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Das ist das Versprechen vom Ende der Lektion vier, eingelöst.
|
|
|
|
[#_put_the_ship_back]
|
|
=== Hol das Schiff zurück
|
|
|
|
Jetzt die fünfte Aufgabe, die, die aus dem alten `onOverlayUpdate` ein Durcheinander gemacht hätte.
|
|
Hol das Kästchen zurück, das du in Lektion drei gesteuert hast, diesmal als Schiff, das nahe am
|
|
unteren Bildschirmrand sitzt und sich nur nach links und rechts bewegt.
|
|
|
|
Es braucht ein paar eigene Werte, oben bei den anderen, und die beiden Eingabe-Callbacks aus
|
|
Lektion drei, ohne die Zweige für oben und unten:
|
|
|
|
[source,lua]
|
|
----
|
|
PLAYER_WIDTH = 20
|
|
PLAYER_HEIGHT = 6
|
|
PLAYER_SPEED = 3
|
|
PLAYER_MARGIN = 6
|
|
|
|
playerX = width // 2
|
|
playerY = height - PLAYER_HEIGHT - PLAYER_MARGIN
|
|
movingLeft = false
|
|
movingRight = false
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = true
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = false
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = false
|
|
end
|
|
end
|
|
----
|
|
|
|
Es zu zeichnen sind zwei Zeilen, also ist es eine Funktion:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawPlayer()
|
|
colorForeground(80, 255, 120)
|
|
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH, playerY + PLAYER_HEIGHT)
|
|
end
|
|
----
|
|
|
|
Beim Bewegen zahlt sich die Lektion aus. In Lektion drei hat es für jede Richtung vier Zeilen mit
|
|
`if` und `elseif` gebraucht, um das Kästchen auf dem Bildschirm zu halten. Diese Idee -- eine Zahl
|
|
zwischen einer Untergrenze und einer Obergrenze halten -- hat einen Namen, und der Name ist
|
|
*clamp*:
|
|
|
|
[source,lua]
|
|
----
|
|
function clamp(value, low, high)
|
|
if value < low then
|
|
return low
|
|
end
|
|
if value > high then
|
|
return high
|
|
end
|
|
return value
|
|
end
|
|
----
|
|
|
|
Drei Rückgaben, und höchstens eine davon passiert je, denn `return` gibt nicht nur eine Antwort
|
|
zurück: Es hält die Funktion auf der Stelle an. Nichts danach läuft. Liegt der Wert unter der
|
|
Untergrenze, gib die Untergrenze zurück und hör auf. Liegt er über der Obergrenze, gib die
|
|
Obergrenze zurück und hör auf. Sonst gib zurück, was hereingekommen ist.
|
|
|
|
[source,lua]
|
|
----
|
|
function movePlayer()
|
|
if movingLeft then
|
|
playerX = playerX - PLAYER_SPEED
|
|
end
|
|
if movingRight then
|
|
playerX = playerX + PLAYER_SPEED
|
|
end
|
|
playerX = clamp(playerX, 0, width - PLAYER_WIDTH - 1)
|
|
end
|
|
----
|
|
|
|
Eine Funktion, die ohne `return` endet, gibt nichts zurück, und das ist `nil`. Das ist richtig für
|
|
`drawPlayer` und `movePlayer`, die aufgerufen werden, um etwas zu tun, nicht um etwas zu
|
|
beantworten. In einer Funktion wie `clamp` ist es ein Fehler: Lass das letzte `return value` weg,
|
|
und jede Position mitten im Bereich kommt als `nil` zurück.
|
|
|
|
[#_where_names_live]
|
|
=== Wo Namen wohnen
|
|
|
|
Jeder Name, den du seit Lektion zwei angelegt hast, war eine *globale Variable*. Eine globale
|
|
Variable gehört dem ganzen Programm: `drift`, `playerX` und `LAYERS` können von jeder Funktion in
|
|
der Datei jederzeit gelesen und geschrieben werden, und sie bleiben da, solange das Spiel läuft.
|
|
|
|
Das klingt bequem, und bei sechs Namen ist es das auch. Der Ärger fängt bei sechzig an. Jede
|
|
globale Variable ist ein Name, den sich das ganze Programm reserviert hat; wenn also irgendeine
|
|
andere Funktion einen Platz für eine waagerechte Position braucht und ihn auch `starX` nennt, sind
|
|
die beiden dieselbe Schublade, und jede überschreibt die Zahl der anderen. Wenn in einer globalen
|
|
Variable etwas Falsches steht, kann es alles in der Datei dort hineingelegt haben, und um
|
|
herauszufinden, was, musst du die ganze Datei lesen.
|
|
|
|
Eine *lokale Variable* ist das Gegenmittel. Schreib das Wort `local` vor einen Namen, wenn du ihn
|
|
zum ersten Mal benutzt, und der Name existiert nur innerhalb der `function`, der Schleife oder des
|
|
`if`, in dem er geschrieben wurde.
|
|
|
|
[source,lua]
|
|
----
|
|
function starAt(star, layer)
|
|
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
|
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
|
return x, y
|
|
end
|
|
----
|
|
|
|
`x` und `y` gehen hier niemanden sonst etwas an. Eine andere Funktion kann ihr eigenes `x` haben,
|
|
und die beiden begegnen sich nie. Wenn das `x` von `starAt` falsch ist, sind die drei Zeilen oben
|
|
die einzigen drei Zeilen, die das gewesen sein können. Ohne das Wort `local` würden diese beiden
|
|
Zeilen still zwei weitere globale Variablen anlegen, und genau das waren `starX` und `starY` die
|
|
ganze Zeit.
|
|
|
|
Jetzt der Teil, der alle überrascht. Tipp diese Funktion ein und setz eine Zeile in
|
|
`onOverlayUpdate`, die dir zeigt, was sie zurückgibt:
|
|
|
|
[source,lua]
|
|
----
|
|
function countUp()
|
|
local counted = 0
|
|
counted = counted + 1
|
|
return counted
|
|
end
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "Count: " .. countUp())
|
|
----
|
|
|
|
`Count: 1`, und es bleibt bei `Count: 1`, solange das Spiel läuft, obwohl die Funktion sechzigmal
|
|
pro Sekunde aufgerufen wird und jedes Mal eins dazuzählt. Eine lokale Variable wird frisch
|
|
angelegt, wenn die Funktion anfängt, und ist weg, wenn die Funktion endet; also wird `counted` als
|
|
`0` geboren, wird zu `1` und stirbt, sechzigmal pro Sekunde, für immer. Eine lokale Variable merkt
|
|
sich nichts. Nimm die beiden wieder heraus, wenn du es dir angesehen hast.
|
|
|
|
Das ist kein Mangel, sondern genau der Sinn: Eine lokale Variable kann nicht vom letzten Aufruf
|
|
vergiftet werden, weil vom letzten Aufruf nichts übrig ist. Wenn du dir doch etwas von einem Aufruf
|
|
zum nächsten merken musst (und `drift` und `playerX` müssen das), muss der Name außerhalb jeder
|
|
Funktion wohnen. Setz ihn mit `local` davor an den Anfang der Datei:
|
|
|
|
[source,lua]
|
|
----
|
|
local drift = 0
|
|
local playerX = width // 2
|
|
----
|
|
|
|
Ein `local` ganz oben in der Datei, außerhalb aller Funktionen, lebt so lange wie das Skript und
|
|
ist für jede Funktion darunter sichtbar. Auf das letzte Wort kommt es an: *darunter*. Eine lokale
|
|
Variable existiert von ihrer eigenen Zeile an abwärts, also kann eine Funktion, die über der Zeile
|
|
steht, die sie anlegt, sie nicht sehen. Dasselbe gilt für die Funktionen selbst, sobald du sie mit
|
|
`local` markierst, und darum hat das fertige Skript die Reihenfolge, die es hat: zuerst die Werte,
|
|
dann deine eigenen Funktionen, dann ganz zum Schluss die Callbacks.
|
|
|
|
Geh jetzt die ganze Datei durch und setz `local` vor jeden Namen, den du angelegt hast -- die
|
|
Konstanten in Großbuchstaben, `width` und `height`, `drift`, die vier Werte der Spielfigur und jede
|
|
deiner eigenen Funktionen. Drei Namen bekommen es nicht: `onInputPressed`, `onInputReleased` und
|
|
`onOverlayUpdate` müssen global bleiben, weil Singe sie über ihren Namen findet und ein lokaler
|
|
Name von außerhalb der Datei unsichtbar ist.
|
|
|
|
Sei ehrlich zu dir, was lokale Variablen nicht reparieren. Lua beschwert sich nicht, wenn du dich
|
|
bei einem Namen vertippst, dem du etwas zuweist -- `movinLeft = true` statt
|
|
`movingLeft = true`. Es legt eine brandneue globale Variable mit deinem Tippfehler als Namen an,
|
|
setzt sie und lässt die echte in Ruhe. Nichts bewegt sich, nichts meldet einen Fehler, und du
|
|
starrst zehn Minuten lang darauf. Davor retten dich lokale Variablen nicht. Den Namen zweimal zu
|
|
lesen schon.
|
|
|
|
[#_the_whole_thing_4]
|
|
=== Das Ganze
|
|
|
|
Hier ist das fertige Programm, mit jedem Stück der Lektion darin.
|
|
|
|
[source,lua]
|
|
----
|
|
local LAYERS = 3
|
|
local STARS_PER_LAYER = 40
|
|
local ACROSS_STEP = 37
|
|
local DOWN_STEP = 61
|
|
local LAYER_SHIFT = 13
|
|
local LAYER_SPEED = 0.5
|
|
local LAYER_BRIGHT = 55
|
|
|
|
local PLAYER_WIDTH = 20
|
|
local PLAYER_HEIGHT = 6
|
|
local PLAYER_SPEED = 3
|
|
local PLAYER_MARGIN = 6
|
|
|
|
local width = overlayGetWidth()
|
|
local height = overlayGetHeight()
|
|
|
|
local drift = 0
|
|
local playerX = width // 2
|
|
local playerY = height - PLAYER_HEIGHT - PLAYER_MARGIN
|
|
local movingLeft = false
|
|
local movingRight = false
|
|
|
|
|
|
local function clamp(value, low, high)
|
|
if value < low then
|
|
return low
|
|
end
|
|
if value > high then
|
|
return high
|
|
end
|
|
return value
|
|
end
|
|
|
|
|
|
local function shadeFor(layer)
|
|
return math.random(60, 90) + layer * LAYER_BRIGHT
|
|
end
|
|
|
|
|
|
local function starAt(star, layer)
|
|
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
|
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
|
return x, y
|
|
end
|
|
|
|
|
|
local function drawStar(x, y, shade)
|
|
colorForeground(shade, shade, shade)
|
|
overlayPlot(x, y)
|
|
end
|
|
|
|
|
|
local function drawLayer(layer)
|
|
for star = 1, STARS_PER_LAYER do
|
|
local x, y = starAt(star, layer)
|
|
drawStar(x, y, shadeFor(layer))
|
|
end
|
|
end
|
|
|
|
|
|
local function drawStars()
|
|
for layer = 1, LAYERS do
|
|
drawLayer(layer)
|
|
end
|
|
end
|
|
|
|
|
|
local function drawPlayer()
|
|
colorForeground(80, 255, 120)
|
|
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH, playerY + PLAYER_HEIGHT)
|
|
end
|
|
|
|
|
|
local function movePlayer()
|
|
if movingLeft then
|
|
playerX = playerX - PLAYER_SPEED
|
|
end
|
|
if movingRight then
|
|
playerX = playerX + PLAYER_SPEED
|
|
end
|
|
playerX = clamp(playerX, 0, width - PLAYER_WIDTH - 1)
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = true
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = false
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = false
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
overlayClear()
|
|
movePlayer()
|
|
drawStars()
|
|
drawPlayer()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Auf zwei Dinge lohnt es sich hinzuweisen, bevor du weitermachst.
|
|
|
|
`local function drawStar(x, y, shade)` markiert die Funktion selbst als lokal. Deine eigenen
|
|
Funktionen sind Namen wie alle anderen, und sie gehören nur dieser Datei. Die drei Callbacks bleiben
|
|
global, damit Singe sie finden kann.
|
|
|
|
Und `onOverlayUpdate` besteht jetzt aus sechs Zeilen, die sagen, was ein Frame ist: die Drift
|
|
zählen, den Bildschirm löschen, die Spielfigur bewegen, den Himmel zeichnen, die Spielfigur
|
|
zeichnen, fertig. Es erledigt fünf Aufgaben, indem es fünf Dinge beim Namen nennt, die jeweils eine
|
|
Aufgabe erledigen, und das ist die Form jeder Spielschleife, die du je schreiben wirst.
|
|
|
|
[#_a_function_is_a_name_for_an_idea]
|
|
=== Eine Funktion ist ein Name für eine Idee
|
|
|
|
`clamp` wird einmal aufgerufen. Sie herauszuziehen hat überhaupt keine Tipparbeit gespart -- vier
|
|
Zeilen kamen heraus, eine ging hinein, und anderswo ist eine Funktion mit neun Zeilen aufgetaucht.
|
|
Es hat sich trotzdem gelohnt.
|
|
|
|
Eine Funktion ist nicht nur ein Weg, dich nicht zu wiederholen. Sie ist ein Weg, einer Idee einen
|
|
Namen zu geben, damit der Code, der die Idee benutzt, den Namen sagen und weitermachen kann.
|
|
„Halte die Spielfigur zwischen dem linken und dem rechten Rand“ ist ein Gedanke, den du einmal
|
|
hattest. `clamp(playerX, 0, width - PLAYER_WIDTH - 1)` ist dieser Gedanke, aufgeschrieben. Vier
|
|
`if`-Anweisungen sind derselbe Gedanke, zerlegt in Stücke, die du jedes Mal, wenn du sie liest, im
|
|
Kopf wieder zusammensetzen musst.
|
|
|
|
Wenn du entscheidest, ob etwas es verdient, eine Funktion zu sein, frag nicht, wie oft du sie
|
|
aufrufen wirst. Frag, ob es einen Namen hat.
|
|
|
|
[#_try_it_5]
|
|
=== Probier es aus
|
|
|
|
. *Benenne einen Parameter um.* Ändere in der Funktion `drawStar` überall `x` und `y` in `across`
|
|
und `down`. Fass nichts außerhalb davon an. Starte es. Finde heraus, warum nichts kaputtgegangen
|
|
ist.
|
|
. *Zeichne sie andersherum.* Vertausch `drawStars()` und `drawPlayer()` in `onOverlayUpdate` und
|
|
sieh dir das Schiff an. Dann stell sie zurück. Jede dieser fünf Zeilen ist eine Aufgabe, und ihre
|
|
Reihenfolge auf dem Bildschirm ist ihre Reihenfolge in der Funktion.
|
|
. *Benutz `clamp` für eine Helligkeit.* Ändere `shadeFor` zu
|
|
`return clamp(math.random(60, 90) + layer * 90, 0, 200)` und sieh dir die drei Ebenen an. Dann
|
|
nimm das `clamp` heraus und sieh noch einmal hin: `colorForeground` hat die Zahlen die ganze Zeit
|
|
still für dich gedeckelt, und jetzt siehst du, wo.
|
|
. *Ruf `drawStar` mit zwei Argumenten auf.* Schreib `drawStar(100, 100)` irgendwo in `drawLayer`
|
|
und starte es. Der Fehler nennt genau das, was gefehlt hat. Lies ihn, bevor du das dritte
|
|
Argument wieder einsetzt.
|
|
. *Verschieb eine lokale Variable.* Nimm die Zeile `local drift = 0` und schieb sie ans Ende der
|
|
Datei, unter alles andere. Starte es, lies den Fehler und denk über das Wort „darunter“ im
|
|
Abschnitt oben nach.
|
|
|
|
[#_break_it_on_purpose_5]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
In einer Datei mit lokalen Variablen kommt es auf die Reihenfolge an, und der Fehler, den du
|
|
bekommst, wenn du diese Reihenfolge durcheinanderbringst, ist nicht der, den du erwartest.
|
|
|
|
Nimm die ganze Funktion `drawStar` und schieb sie ans *Ende* des Skripts, unter `onOverlayUpdate`.
|
|
Alles ist noch da, gleich geschrieben. Starte es:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': functions/functions.singe:50: attempt to call a nil value (global 'drawStar')
|
|
stack traceback:
|
|
functions/functions.singe:50: in upvalue 'drawLayer'
|
|
functions/functions.singe:57: in upvalue 'drawStars'
|
|
functions/functions.singe:101: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Das sieht anders aus als der Fehler in Lektion eins, weil es eine andere Art von Fehler ist. Der
|
|
Fehler aus Lektion eins wurde erwischt, bevor das Skript lief. Dieser hier wurde erwischt, während
|
|
es lief, und darum nennt Singe die Funktion, in der es gerade steckte, und darum steht darunter ein
|
|
*Traceback*: die Liste, wer wen aufgerufen hat, das Neueste zuerst. Zeile 50 liegt in `drawLayer`,
|
|
das von Zeile 57 in `drawStars` aufgerufen wurde, das von Zeile 101 in `onOverlayUpdate`
|
|
aufgerufen wurde. Deine Zeilennummern weichen ab, wenn deine Datei nicht genau mit der oben
|
|
übereinstimmt, und die Engine hängt darunter vielleicht noch eine oder zwei eigene Zeilen an. Das
|
|
Wort `upvalue` ist Luas Name für eine lokale Variable, die sich eine Funktion aus der Datei um sie
|
|
herum geliehen hat; lies vorerst darüber hinweg.
|
|
|
|
Die Beschwerde selbst lohnt es sich auswendig zu lernen. „Attempt to call a nil value“ heißt, dass
|
|
du Klammern hinter einen Namen gesetzt hast, in dem nichts stand. Das Wort `global` vor `drawStar`
|
|
ist der Teil, der alles verrät: Du hast `drawStar` als lokal markiert, und als Lua `drawLayer` gelesen und
|
|
nirgendwo darüber eine lokale Variable dieses Namens gefunden hat, blieb nur noch, es mit einer
|
|
globalen zu versuchen -- und eine globale namens `drawStar` gibt es auch nicht. Setz die Funktion
|
|
wieder über den Code, der sie aufruft, und der Fehler verschwindet.
|
|
|
|
[#_what_you_learned_5]
|
|
=== Was du gelernt hast
|
|
|
|
* Du kannst eigene Funktionen schreiben, und nichts führt sie aus außer deinen eigenen Aufrufen.
|
|
* Die Namen in den Klammern einer Funktion sind Parameter, und sie sind die eigenen Kopien der
|
|
Funktion von dem, was der Aufrufer übergeben hat.
|
|
* `return` gibt einen Wert zurück und hält die Funktion an der Stelle an, und eine Funktion, die
|
|
ohne ein solches endet, gibt `nil` zurück.
|
|
* Ein Aufruf kann ein Argument eines anderen Aufrufs sein, und der innere passiert zuerst.
|
|
* Lua kann mehr als einen Wert zurückgeben, und du fängst sie mit mehreren Namen auf, durch Kommas
|
|
getrennt.
|
|
* `local` legt einen Namen an, der nur dort existiert, wo er geschrieben wurde.
|
|
* Eine lokale Variable in einer Funktion wird bei jedem Aufruf frisch angelegt und merkt sich
|
|
nichts vom letzten.
|
|
* Eine lokale Variable ist nur unterhalb der Zeile sichtbar, die sie anlegt, also kommen Werte und
|
|
deine eigenen Funktionen über die Callbacks, die sie benutzen.
|
|
* Callbacks bleiben global, weil Singe sie über ihren Namen findet.
|
|
* Eine Funktion lohnt sich, wenn das, was sie tut, einen Namen hat, auch wenn du sie nur einmal
|
|
aufrufst.
|
|
|
|
[#_next_time_5]
|
|
=== Beim nächsten Mal
|
|
|
|
Deine hundertzwanzig Sterne sind immer noch hundertzwanzig Kopien von drei Ideen. Nichts im
|
|
Programm ist *ein Stern*: Es gibt eine Zahl und etwas Rechnerei, die aus der Zahl eine Position
|
|
macht. Du kannst nicht sagen „dieser hier ist näher, also fällt er schneller und leuchtet heller“,
|
|
weil es kein „dieser hier“ gibt, über das du es sagen könntest, und du kannst keinen Stern unten
|
|
herausfallen und durch einen neuen ersetzen lassen, weil da nichts ist, was man entfernen könnte.
|
|
|
|
Lektion sechs gibt dir das Ding, das einen Stern festhält, und dann einen Weg, hundertzwanzig davon
|
|
festzuhalten und einen hunderteinundzwanzigsten hinzuzufügen, während das Spiel läuft. Es ist die
|
|
nützlichste Lektion in diesem Teil des Buchs.
|