739 lines
28 KiB
Text
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.
|