singe/docs/de/lessons/06-tables.adoc

739 lines
28 KiB
Text

[#_lesson_6_lists_of_things]
== Lektion 6: Listen von Dingen
image::learn/06-tables.png[Die fertige Lektion, 480]
Du hast hundertzwanzig Sterne, und keiner davon existiert. Nirgends in deinem Programm gibt es einen
Stern: Es gibt eine Schleife, die hundertzwanzigmal läuft, und bei jedem Durchlauf rechnet sie aus
der Nummer des Sterns und seiner Ebene zwei Zahlen aus, setzt ein Pixel und vergisst alles. Die
drei Ebenen sind da, um das eine vorzutäuschen, was diese Anordnung nicht kann: einen Stern anders
sein zu lassen als seinen Nachbarn.
Diese Lektion gibt dir, was fehlt. Du lernst, wie du eine Liste von Dingen aufbewahrst, wie du ein
Ding machst, das mehrere benannte Werte auf einmal hält, und dann, wie du beides zu einer Liste von
Dingen zusammensetzt, von denen sich jedes seine eigene Position, Geschwindigkeit und Helligkeit
merkt. Es ist die längste Lektion in diesem Teil des Buchs und die, auf die sich alles Spätere
stützt. Lass dir Zeit damit.
[#_start_a_new_folder_5]
=== Leg einen neuen Ordner an
Ein Ordner namens `tables`, darin eine Datei namens `tables.singe`, und in einem Terminal läuft
`Singe -R tables`. Kopier das fertige Programm aus Lektion fünf hinein: das Sternenfeld, das
Raumschiff und alles andere.
[#_a_list]
=== Eine Liste
Füg diese Zeile oben in `tables.singe` ein, unter den anderen Werten, und die drei Aufrufe von
`overlayPrint` in `onOverlayUpdate`, über dem `return`.
[source,lua]
----
local names = { "Ada", "Grace", "Alan" }
----
[source,lua]
----
overlayPrint(2, 2, names[1])
overlayPrint(2, 3, names[3])
overlayPrint(2, 4, "There are " .. #names .. " names.")
----
Starte es. `Ada` in einer Zeile, `Alan` in der nächsten und `There are 3 names.` darunter.
Die geschweiften Klammern in der ersten Zeile machen eine *Tabelle*. Eine Tabelle ist der eine
Behälter, den Lua hat, und alles, wovon du je mehr als eins aufbewahrst, kommt in eine. Hier hält
sie drei Strings hintereinander, und diese Form nennt man eine *Liste*.
Die eckigen Klammern in den nächsten Zeilen holen einen Eintrag heraus. `names[1]` ist der erste
Eintrag, `names[3]` der dritte, und die Zahl in den Klammern heißt *Index*.
Bei dieser Zahl trennen sich die Wege von Lua und fast jeder anderen Sprache, der du begegnen
wirst. *Lua zählt ab eins.* C, Java, Python, JavaScript und die meisten anderen zählen ab null,
also ist ihr erster Eintrag Eintrag null und ihr dritter Eintrag Index zwei. Der erste Eintrag von
Lua ist Eintrag eins, und das ist genau das eine, worüber Programmierer stolpern, die von woanders
herkommen. Du kommst von nirgendwo, also hast du es leichter: Der erste ist Nummer eins.
`names[0]` ist übrigens kein Fehler. Dort ist nichts gespeichert, also ist es `nil` -- der
Nichts-Wert aus Lektion drei. Ein Index, der nie gefüllt wurde, liefert beim Lesen immer `nil`, und
das ist eine Freundlichkeit, bis zu dem Moment, in dem du versuchst, es zu benutzen.
Das `#` vor dem Namen einer Tabelle ist für eine Weile das letzte neue Satzzeichen. Es gibt dir,
wie viele Einträge in der Liste sind, also ist `#names` gleich `3`. Weil die Anzahl und der letzte
Index dieselbe Zahl sind, ist `names[#names]` immer der letzte Eintrag, egal wie lang die Liste
wird.
[#_adding_and_taking_away]
=== Hinzufügen und Wegnehmen
Eine Liste, die du komplett ausschreiben musst, nützt einem Spiel nicht viel. Mit diesen beiden
Aufrufen ändert sich eine Liste, während das Programm läuft.
[source,lua]
----
table.insert(names, "Katherine")
----
Das hängt `Katherine` hinten an, und `#names` ist jetzt `4`. `table.insert` ist eine Funktion wie
jede andere, und der Punkt in der Mitte ihres Namens bedeutet, dass sie in einer Tabelle namens
`table` wohnt, die Lua mitbringt -- einer Tabelle voller Funktionen, die mit Tabellen arbeiten. Du
musst sie weder anlegen noch laden. Sie ist da.
Gib `table.insert` drei Argumente, und das mittlere sagt, wohin der neue Eintrag kommt.
[source,lua]
----
table.insert(names, 1, "Edsger")
----
`Edsger` kommt vorne hinein, und alles, was schon da war, rückt auf, um Platz zu machen: `Ada` war
Eintrag eins und ist jetzt Eintrag zwei. Nichts wird überschrieben, und nichts geht verloren.
Einen Eintrag herauszunehmen ist das Spiegelbild.
[source,lua]
----
table.remove(names, 2)
----
Eintrag zwei verschwindet, und alles dahinter rückt nach, um die Lücke zu schließen, sodass die
Liste nie ein Loch in der Mitte hat. `table.remove` gibt außerdem den Eintrag zurück, den es
herausgenommen hat, also wirft `local gone = table.remove(names, 2)` ihn weg und sagt dir in einer
Zeile, was er war. Lass die Zahl ganz weg -- `table.remove(names)` -- und es nimmt den letzten
Eintrag, und so benutzt du eine Liste am schnellsten als Stapel.
Setz diese drei Aufrufe nacheinander in dein Skript, starte es nach jedem und sieh zu, wie sich die
Anzahl in der dritten Zeile ändert.
[#_walking_a_list]
=== Eine Liste durchlaufen
Eine Möglichkeit, jeden Eintrag zu besuchen, kennst du schon, denn du benutzt sie seit Lektion
vier:
[source,lua]
----
for i = 1, #names do
overlayPrint(2, i + 1, names[i])
end
----
Das funktioniert. Bei eins anfangen, bei der Anzahl aufhören, den Zähler als Index benutzen. Es
gibt einen besseren Weg, und so wirst du es ab jetzt schreiben:
[source,lua]
----
for i, name in ipairs(names) do
overlayPrint(2, i + 1, name)
end
----
`ipairs` läuft eine Liste ab Eintrag eins durch, bis sie zu Ende ist, und gibt deiner Schleife bei
jedem Durchlauf zwei Dinge: den Index und den Eintrag selbst. Auf das zweite kommt es an. Mit dem
zählenden `for` bekommst du eine Zahl und musst mit `names[i]` zurück zur Tabelle, um
herauszufinden, was dort steht; mit `ipairs` hast du den Eintrag schon in der Hand, unter dem
Namen, den du als zweiten hinschreibst.
Außerdem kann man es schwerer falsch machen. `for i = 1, #names` ist eine Stelle, an der man sich
leicht um eins verzählt -- fang bei null an, und du gibst ein `nil` aus, hör bei `#names - 1` auf,
und dir fehlt der letzte -- und in `ipairs` steht überhaupt keine Zahl, die man falsch machen
könnte.
Meistens willst du gar nicht den Index, nur den Eintrag. Schreib den Index als einzelnen
Unterstrich, und fertig:
[source,lua]
----
for _, name in ipairs(names) do
----
`_` ist ein ganz gewöhnlicher Name, und nichts in Lua behandelt ihn besonders. Es ist eine
Gewohnheit, mit der Programmierer sagen: „Hier kommt etwas an, und ich werde es nicht benutzen.“
Wer deine Schleife liest, weiß auf einen Blick, dass der Index darin keine Rolle spielt.
Lösch jetzt die Versuche mit `names` und mit ihnen die Zeilen mit `overlayPrint`. Du weißt, was
eine Liste ist. Zeit, etwas Besseres als einen String hineinzulegen.
[#_a_table_can_also_be_a_record]
=== Eine Tabelle kann auch ein Datensatz sein
Ein Stern muss sich vier Dinge merken: wo er waagerecht ist, wo er senkrecht ist, wie schnell er
fällt und wie hell er ist. Vier getrennte Listen, die du alle von Hand im Gleichschritt halten
musst, wären furchtbar. Mach stattdessen eine Tabelle und leg Namen hinein.
[source,lua]
----
local star = {}
star.x = 40
star.y = 0
star.speed = 2
star.shade = 180
----
Die leeren geschweiften Klammern machen eine leere Tabelle. Die vier Zeilen danach legen Werte
hinein, unter Namen statt unter Nummern, und mit dem Punkt sagst du, welchen Namen du meinst.
`star.x` liest man als das `x` von `star`, und es verhält sich genau wie jede andere Variable:
Lies es, weise ihm etwas zu, zähl etwas dazu.
[source,lua]
----
star.y = star.y + star.speed
----
Eine so benutzte Tabelle -- eine feste Menge benannter Werte, die ein Ding beschreiben -- ist ein
*Datensatz*, und die Namen darin sind seine *Felder*. Es ist dieselbe Art Tabelle wie die Liste
oben. Der einzige Unterschied ist, dass man die Einträge einer Liste über eine Nummer findet und
die Felder eines Datensatzes über einen Namen, und eine Tabelle kann beides gleichzeitig, falls du
das je willst.
Der Punkt ist eine Kurzschreibweise. `star.x` und `star["x"]` bedeuten genau dasselbe, und das
solltest du einmal gesehen haben: Die eckigen Klammern nehmen einen Namen genauso gern wie eine
Zahl, also sind eine Liste und ein Datensatz wirklich eine Idee mit zwei Hüten. Benutz den Punkt.
Er liest sich besser.
Ein Feld, das du nie gesetzt hast, ist `nil`, genau wie ein Index, den du nie gefüllt hast. Es gibt
nirgends eine Liste erlaubter Feldnamen, und nichts prüft deine Rechtschreibung, also ist
`star.sped` kein Fehler -- es ist `nil`, und was dann passiert, lernst du am Ende dieser Lektion
kennen.
Vier Zeilen zu schreiben, um vier Felder zu füllen, wird schnell langweilig. Du kannst die Felder
gleich beim Anlegen der Tabelle in die geschweiften Klammern schreiben:
[source,lua]
----
local star = {
x = 40,
y = 0,
speed = 2,
shade = 180
}
----
Dieselbe Tabelle, eine Anweisung. Die Kommas trennen die Felder, hinter dem letzten braucht es
kein Komma, und die Zeilen sind nur so verteilt, damit man sie leicht lesen kann.
[#_a_list_of_records]
=== Eine Liste von Datensätzen
Hier ist die ganze Idee dieser Lektion in zwei Zeilen:
[source,lua]
----
local stars = {}
table.insert(stars, star)
----
Eine Liste kann alles halten, auch eine Tabelle. Eine Liste von Datensätzen ist also eine Liste
von Dingen, von denen sich jedes alles über sich selbst merkt. `stars[1]` ist ein ganzer Stern, und
`stars[1].y` ist, wie weit unten auf dem Bildschirm dieser Stern steht. Fast jedes Spiel, das du je
schreibst, ist eine Handvoll solcher Listen: die Gegner, die Schüsse, die Extras und die fallenden
Blöcke.
Eine Sache über Tabellen musst du wissen, bevor du den Code schreibst, und sie erwischt jeden
einmal. Wenn du einer Funktion eine Tabelle übergibst, wird sie nicht kopiert. Eine Zahl wird
kopiert -- darum konnte `half` in Lektion fünf ihre `number` zerhacken, ohne `drift` zu stören --
aber eine Tabelle wird geteilt. Beide Namen verweisen auf dieselbe Tabelle, und eine Änderung über
den einen ist über den anderen zu sehen.
[source,lua]
----
local star = stars[3]
star.y = star.y + 1
----
Das bewegt den dritten Stern in der Liste. Keine Kopie davon: ihn selbst. Genau das willst du hier,
und genau das lässt den nächsten Abschnitt funktionieren, aber denk daran, wenn eine Funktion, die
du geschrieben hast, etwas ändert, bei dem du das nicht erwartet hast.
[#_rebuild_the_star_field]
=== Bau das Sternenfeld neu
Die Rechnerei fliegt raus. Ein Stern wird gleich zu einem Ding.
Lösch `starAt`, `shadeFor`, `drawLayer` und das alte `drawStar` und mit ihnen die Konstanten, die
sie benutzt haben: `LAYERS`, `STARS_PER_LAYER`, `ACROSS_STEP`, `DOWN_STEP`, `LAYER_SHIFT` und
`LAYER_SPEED`. Die Variable `drift` fliegt auch raus, und ebenso die Zeile mit `drift` oben in
`onOverlayUpdate`. `LAYER_BRIGHT` bleibt, unter einem Namen, der keine Ebenen mehr erwähnt, und
zwei Werte kommen dazu:
[source,lua]
----
local STAR_COUNT = 120
local STAR_SHADE = 55
local BURST_COUNT = 10
----
[source,lua]
----
local stars = {}
----
Hundertzwanzig hattest du vorher auch: drei Ebenen zu je vierzig. Die Liste fängt leer an, und das
hier füllt sie:
[source,lua]
----
local function newStar(y)
local speed = math.random(1, 3)
local star = {
x = math.random(0, width - 1),
y = y,
speed = speed,
shade = math.random(60, 90) + speed * STAR_SHADE
}
return star
end
----
`newStar` macht einen Stern und gibt ihn zurück. Sieh dir das letzte Feld an und vergleich es mit
`shadeFor` aus Lektion fünf: dieselbe Rechnung, mit der eigenen Geschwindigkeit des Sterns dort, wo
früher die Nummer der Ebene stand. Die Ebenen sind nicht so sehr verschwunden als vielmehr
aufgelöst. Jeder Stern sucht sich jetzt seine eigene Geschwindigkeit aus, ein, zwei oder drei Pixel
pro Frame, und ein Stern, der schneller fällt, ist heller; das ist die ganze Illusion von Tiefe,
und sie kostet eine Zeile.
Das anfängliche `y` ist ein Parameter, weil die beiden Aufrufer Verschiedenes wollen. Die Sterne,
die beim Start des Spiels entstehen, sollen über den ganzen Bildschirm verstreut sein; die, die
später entstehen, kommen oben herein.
[source,lua]
----
local function makeStars()
for number = 1, STAR_COUNT do
table.insert(stars, newStar(math.random(0, height - 1)))
end
end
----
Noch ruft nichts `makeStars` auf. Füg ganz unten in der Datei eine Zeile hinzu, nach allem anderen
und in überhaupt keiner Funktion:
[source,lua]
----
makeStars()
----
Über diese Zeile gibt es noch mehr zu sagen, und das wartet bis zum Ende der Lektion. Jetzt das
Zeichnen, das jedes Mal kürzer wird, wenn du es anfasst:
[source,lua]
----
local function drawStar(star)
colorForeground(star.shade, star.shade, star.shade)
overlayPlot(star.x, star.y)
end
local function drawStars()
for _, star in ipairs(stars) do
drawStar(star)
end
end
----
`drawStar` hat in Lektion fünf drei Zahlen bekommen und bekommt jetzt einen Stern. Alles, was es
wissen muss, steckt im Stern, und genau darum gibt es überhaupt Datensätze: ein Ding zum
Herumreichen statt vier, und keine Gefahr, sie in der falschen Reihenfolge zu übergeben. Und
`drawStars` hat seine Schleife über die Ebenen verloren, zusammen mit jedem Interesse daran, wie
viele Sterne es gibt oder woher sie kommen.
[#_stars_that_come_and_go]
=== Sterne, die kommen und gehen
Jetzt der Teil, den das alte Sternenfeld überhaupt nicht konnte. Sterne fallen mit ihrer eigenen
Geschwindigkeit, verschwinden unten und werden oben ersetzt.
[source,lua]
----
local function moveStars()
for i = #stars, 1, -1 do
local star = stars[i]
star.y = star.y + star.speed
if star.y >= height then
table.remove(stars, i)
end
end
while #stars < STAR_COUNT do
table.insert(stars, newStar(0))
end
end
----
Ruf sie in `onOverlayUpdate` auf, in der Zeile, in der früher `drift = drift + 1` stand.
Jeder Stern bewegt sich um seine eigene `speed` nach unten, und das ist die Zeile, die Lektion fünf
nicht schreiben konnte. Das `while` unten ist das Auffüllen: Solange es weniger als hundertzwanzig
Sterne gibt, mach oben auf dem Bildschirm einen neuen. Es läuft so oft, wie es muss, und hört dann
auf, und das ist das richtige Werkzeug, wenn du nicht weißt, wie viele fehlen.
Die `for`-Zeile oben zählt rückwärts, mit der Schrittweite, die du in Lektion vier kennengelernt
hast. `for i = #stars, 1, -1` besucht zuerst den letzten Eintrag und arbeitet sich zurück bis
Eintrag eins. Das ist keine Verzierung. Es ist der einzige sichere Weg, das zu tun, was diese
Schleife tut, und der nächste Abschnitt erklärt, warum.
[#_never_remove_going_forwards]
=== Nie vorwärts entfernen
Schreib dieselbe Schleife vorwärts, und sie ist kaputt:
[source,lua]
----
for i = 1, #stars do
local star = stars[i]
star.y = star.y + star.speed
if star.y >= height then
table.remove(stars, i)
end
end
----
Geh sie von Hand mit vier Sternen durch, und nimm an, dass die Sterne zwei und drei beide unten
angekommen sind.
* `i` ist `1`. Stern eins ist in Ordnung.
* `i` ist `2`. Stern zwei ist unten hinaus, also fliegt er raus. Alles rückt nach: Der Stern, der
drei war, ist jetzt zwei, und der Stern, der vier war, ist jetzt drei.
* `i` ist `3`. Das ist der Stern, der früher vier war. *Stern drei wurde nie angesehen.* Das
Entfernen von Eintrag zwei hat ihn auf Platz zwei rücken lassen, und die Schleife war schon bei
Platz zwei gewesen und weitergezogen.
* `i` ist `4`. Es gibt keinen Eintrag vier mehr. `stars[4]` ist `nil`.
Zwei getrennte Fehler aus einem kleinen Versehen. Einträge werden übersprungen, also wird ein
Stern, der unten angekommen war, diesmal nicht einmal angesehen. Ein Stern, der einen Frame länger
überlebt, schadet niemandem, aber dieselbe Schleife über eine Liste von Gegnern, die erst das
Ansehen tötet, ist ein Fehler, dem du eine Stunde lang hinterherjagst. Der zweite Fehler ist
lauter. Die Liste ist jetzt kürzer als die Anzahl, mit der die Schleife angefangen hat, weil
`#stars` einmal ausgerechnet wurde, vor jedem Entfernen, und so greifen die letzten Durchläufe der
Schleife über das Ende der Liste hinaus:
----
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to index a nil value (local 'star')
stack traceback:
tables/tables.singe:60: in upvalue 'moveStars'
tables/tables.singe:124: in function 'onOverlayUpdate'
----
„Attempt to index a nil value“ heißt, dass du einen Punkt hinter etwas gesetzt hast, in dem nichts
war. Das `(local 'star')` am Ende nennt es beim Namen: `star` war `nil`, also hatte `star.y` keinen
Ort, an dem es nachsehen konnte.
Rückwärts zu laufen behebt beide Fehler auf einmal, und es lohnt sich zu sehen, warum, statt es
einfach zu glauben. Wenn du Eintrag `i` entfernst, rückt alles hinter `i` einen Platz nach unten
-- und wenn du rückwärts läufst, ist alles hinter `i` genau das, was du schon besucht hast. Was
noch kommt, die Einträge `i - 1` bis hinunter zu `1`, hat sich überhaupt nicht bewegt. Dass die
Liste hinter dir schrumpft, kann dir auch nicht schaden, denn du läufst auf Eintrag eins zu, und
Eintrag eins ist immer da.
Die Regel, die du für den Rest deines Lebens benutzen wirst: *Wenn eine Schleife Einträge aus der
Liste entfernen könnte, die sie durchläuft, lauf sie rückwärts durch.*
`ipairs` ist keine Ausnahme davon. Einträge mitten in einer `ipairs`-Schleife zu entfernen geht
auf dieselbe Weise schief. `ipairs` ist zum Ansehen da, und das rückwärts laufende `for` zum
Ändern.
[#_add_some_yourself]
=== Füg selbst welche hinzu
Noch ein Zweig in `onInputPressed`, und du kannst zusehen, wie die Liste wächst, während das Spiel
läuft. Das kommt zu den anderen beiden:
[source,lua]
----
elseif what == SWITCH_BUTTON1 then
addBurst()
----
[source,lua]
----
local function addBurst()
for number = 1, BURST_COUNT do
table.insert(stars, newStar(0))
end
end
----
`addBurst` kommt über die Callbacks, zu deinen anderen Funktionen. Und damit du siehst, wie sich
die Länge der Liste ändert, bring die Anzahl in `onOverlayUpdate` auf den Bildschirm:
[source,lua]
----
overlayPrint(0, 0, "Stars: " .. #stars)
----
Drück den Knopf -- die Leertaste, außer du hast geändert, woher `SWITCH_BUTTON1` kommt -- ein
Dutzend Mal. Die Anzahl steigt mit jedem Drücken um zehn, und der Himmel wird dichter, und solange
du ihn gedrückt hältst, wiederholt sich nichts: `onInputPressed` erfährt es einmal, wenn der Knopf
nach unten geht. Dann sieh zu, wie die Anzahl von selbst auf hundertzwanzig zurückgeht, während
die zusätzlichen Sterne unten ankommen und entfernt werden, denn das Auffüllen füllt die Liste
immer nur bis `STAR_COUNT` auf und kürzt sie nie.
Weder dem Zeichnen noch dem Bewegen noch dem Entfernen musste jemand sagen, dass sich die Zahl der
Sterne geändert hat. Sie arbeiten alle mit der Liste.
[#_the_whole_thing_5]
=== Das Ganze
[source,lua]
----
local STAR_COUNT = 120
local STAR_SHADE = 55
local BURST_COUNT = 10
local PLAYER_WIDTH = 20
local PLAYER_HEIGHT = 6
local PLAYER_SPEED = 3
local PLAYER_MARGIN = 6
local width = overlayGetWidth()
local height = overlayGetHeight()
local stars = {}
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 newStar(y)
local speed = math.random(1, 3)
local star = {
x = math.random(0, width - 1),
y = y,
speed = speed,
shade = math.random(60, 90) + speed * STAR_SHADE
}
return star
end
local function makeStars()
for number = 1, STAR_COUNT do
table.insert(stars, newStar(math.random(0, height - 1)))
end
end
local function addBurst()
for number = 1, BURST_COUNT do
table.insert(stars, newStar(0))
end
end
local function moveStars()
for i = #stars, 1, -1 do
local star = stars[i]
star.y = star.y + star.speed
if star.y >= height then
table.remove(stars, i)
end
end
while #stars < STAR_COUNT do
table.insert(stars, newStar(0))
end
end
local function drawStar(star)
colorForeground(star.shade, star.shade, star.shade)
overlayPlot(star.x, star.y)
end
local function drawStars()
for _, star in ipairs(stars) do
drawStar(star)
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
elseif what == SWITCH_BUTTON1 then
addBurst()
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
movingLeft = false
elseif what == SWITCH_RIGHT then
movingRight = false
end
end
function onOverlayUpdate()
overlayClear()
movePlayer()
moveStars()
drawStars()
drawPlayer()
overlayPrint(0, 0, "Stars: " .. #stars)
return OVERLAY_UPDATED
end
makeStars()
----
[#_what_just_happened_3]
=== Was gerade passiert ist
Das Programm ist kürzer als das aus Lektion fünf, und es tut etwas, woran Lektion fünf nicht
einmal denken konnte. Drei Stücke sind einen zweiten Blick wert.
[source,lua]
----
makeStars()
----
Diese einzelne Zeile ganz unten in der Datei steht in keiner Funktion, und man übersieht sie
leicht. Lua liest deine Datei von oben nach unten, wenn Singe sie lädt, und eine Zeile ganz außen
in der Datei läuft sofort an Ort und Stelle, einmal, bevor die Engine irgendetwas aufgerufen hat.
Alles darüber sind Definitionen -- hier ist ein Wert, hier ist eine Funktion -- und das hier ist
die eine Anweisung, die beim Laden passiert. Sie muss ganz unten stehen, weil es `makeStars` erst
gibt, wenn die Zeile gelesen wurde, die es erzeugt.
[source,lua]
----
local star = stars[i]
star.y = star.y + star.speed
----
`star` ist eine lokale Variable, die bei jedem Durchlauf der Schleife neu entsteht, und sie hält
dieselbe Tabelle, die in der Liste liegt -- keine Kopie davon. `star.y` zu ändern bewegt den Stern,
der in der Liste steht. Würden Tabellen kopiert, würde diese Schleife pro Frame hundertzwanzig
Kopien bewegen, und auf dem Bildschirm würde sich nie etwas ändern.
[source,lua]
----
for _, star in ipairs(stars) do
drawStar(star)
end
----
Vergleich das mit Lektion vier: zwei verschachtelte Schleifen, zwei Rechnungen pro Stern, ein
Rest, damit jeder auf dem Bildschirm bleibt, und drei Ebenen, um die Vielfalt vorzutäuschen, die du
jetzt umsonst bekommst. Das Zeichnen weiß nicht mehr und kümmert sich nicht mehr darum, wie die
Sterne dorthin gekommen sind, wo sie sind, wie viele es gibt oder was als Nächstes mit ihnen
passiert. Jeder Teil des Programms erledigt eine Aufgabe, und die Liste ist das, worauf sich alle
einigen.
[#_try_it_6]
=== Probier es aus
. *Mehr Tiefe.* Ändere `math.random(1, 3)` in `math.random(1, 6)` und `STAR_SHADE` in `25`. Mehr
Geschwindigkeiten, mehr Helligkeitsstufen, ein tieferer Himmel.
. *Weniger Sterne, größerer Schwung.* Setz `STAR_COUNT` auf `10` und `BURST_COUNT` auf `100`,
dann drück den Knopf drei- oder viermal. Sieh zu, wie die Anzahl auf dem Bildschirm steigt, und
miss, wie lange es dauert, bis sie wieder abgeflossen ist.
. *Gib einem Stern ein weiteres Feld.* Füg `age = 0` in `newStar` hinzu, zähl in `moveStars` eins
zu `star.age` dazu und bring `stars[1].age` neben der Anzahl auf den Bildschirm. Sonst muss sich
nichts im Programm ändern, damit es eine neue Tatsache über jeden Stern mitträgt.
. *Hör auf, sie zu entfernen.* Lösch die drei Zeilen des Blocks `if star.y >= height` aus
`moveStars` und starte es. Warte zehn Sekunden, dann lies die Anzahl auf dem Bildschirm und sieh
dir den Himmel an. Finde heraus, wie beides gleichzeitig wahr sein kann.
. *Mach es vorwärts.* Ändere `for i = #stars, 1, -1` in `for i = 1, #stars` und lass es laufen, bis
es umfällt. Dann lies den Abschnitt oben noch einmal, mit dem echten Fehler vor Augen.
[#_break_it_on_purpose_6]
=== Mach es absichtlich kaputt
Schreib einen Feldnamen falsch. Ändere in `moveStars` das `star.speed` in `star.sped`:
[source,lua]
----
star.y = star.y + star.sped
----
Starte es:
----
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to perform arithmetic on a nil value (field 'sped')
stack traceback:
tables/tables.singe:60: in upvalue 'moveStars'
tables/tables.singe:124: in function 'onOverlayUpdate'
----
Deine Zeilennummern werden nicht genau diese sein, außer deine Datei stimmt Zeile für Zeile mit der
oben überein. Was du liest, sind der Name der Datei, die Zahl dahinter und die Beschwerde.
Lies zuerst das Ende der ersten Zeile. Mit `field 'sped'` sagt dir Lua, welcher Name leer ausging,
und es sagt dir, dass es ein Feld war -- etwas hinter einem Punkt -- und keine eigene Variable von
dir. „Attempt to perform arithmetic on a nil value“ ist das, was es gerade tat, als es das merkte:
`nil` zu einer Zahl addieren, und das geht nicht.
Es gab keine Beschwerde, als die Tabelle ohne ein Feld `sped` gemacht wurde, und keine in dem
Moment, in dem du danach gefragt hast. Eine Tabelle gibt für jeden Namen, den sie nicht hat,
kommentarlos `nil` heraus. Der Fehler kommt später, in der ersten Zeile, die versucht, das Nichts,
das du bekommen hast, zu *benutzen*, und die kann weit von dem Tippfehler entfernt sein. Wenn du
`nil value (field 'something')` siehst, such die Stelle, an der dieser Name anders geschrieben
wurde.
[#_what_you_learned_6]
=== Was du gelernt hast
* Geschweifte Klammern machen eine Tabelle, den einen Behälter, den Lua hat. Eine Tabelle, die als
Liste benutzt wird, hält Einträge hintereinander, und eckige Klammern mit einem Index holen einen
heraus.
* Lua zählt ab eins. Die meisten anderen Sprachen zählen ab null.
* `#list` ist die Anzahl der Einträge darin, und `list[#list]` ist der letzte.
* `table.insert` fügt einen Eintrag hinzu, am Ende oder an einer Stelle, die du nennst;
`table.remove` nimmt einen heraus und schließt die Lücke.
* Ein Index oder ein Feld, das nie gefüllt wurde, ist `nil`, und nichts warnt dich, bis du es
benutzt.
* `ipairs` läuft eine Liste durch und gibt dir den Index und den Eintrag; `_` ist die übliche
Schreibweise für einen Index, den du nicht willst.
* Eine Tabelle, die als Datensatz benutzt wird, hält benannte Felder, die du mit einem Punkt
erreichst.
* In einer Liste von Datensätzen bewahrt ein Spiel seine Gegner auf, seine Schüsse und alles
andere, wovon es mehr als eins gibt.
* Tabellen werden geteilt, nicht kopiert, wenn du sie herumreichst.
* Wenn eine Schleife Einträge aus der Liste entfernen kann, die sie durchläuft, lauf sie rückwärts
durch.
[#_next_time_6]
=== Beim nächsten Mal
Du hast jetzt alle Teile eines Spiels: Dinge, die sich bewegen, ein Raumschiff, das auf die
Steuerung hört, Entscheidungen, Schleifen, Funktionen und eine Liste, in die Dinge hinein- und aus
der sie wieder hinausgehen können, während das Spiel läuft.
Lektion sieben setzt sie zu einem Spiel zusammen, das du verlieren kannst. Du steuerst am unteren
Rand des Bildschirms entlang, von oben fallen Blöcke, und du gehst ihnen aus dem Weg -- und die
fallenden Blöcke sind eine Liste von Datensätzen, gemacht, bewegt und entfernt genau wie deine
Sterne.