491 lines
22 KiB
Text
491 lines
22 KiB
Text
[#_lesson_11_hitting_things]
|
|
== Lektion 11: Dinge treffen
|
|
|
|
image::learn/11-hitting-things.png[Die fertige Lektion, 480]
|
|
|
|
Dein Spiel aus Lektion zehn funktioniert. Schüsse zerbrechen Felsen, Felsen kosten Leben, und die
|
|
Explosionen knallen an den richtigen Stellen. Sieh aber genau hin, und du merkst, dass es
|
|
schummelt, und zwar in beide Richtungen. Ein Felsen, der an der Flügelspitze vorbeigleitet, kostet
|
|
ein Leben. Ein Schuss, der neben einem Felsen durch den leeren Himmel fliegt, zerbricht ihn
|
|
trotzdem.
|
|
|
|
Zwei Dinge in einem Spiel berühren sich, wenn die Zahlen das sagen. Niemand sieht sich die Bilder
|
|
an. Diese Lektion bringt die Zahlen mit dem in Einklang, was der Spieler sieht: Sie tauscht deinen
|
|
selbst geschriebenen Test gegen den der Engine, verkleinert die geprüften Boxen, bis sie zu den
|
|
Dingen darin passen, und gibt dir einen Schalter, der dir diese Boxen zeigt. Das Bild oben ist das
|
|
Spiel mit eingeschaltetem Schalter.
|
|
|
|
[#_where_you_are]
|
|
=== Wo du stehst
|
|
|
|
Das Spiel fragt an genau zwei Stellen „berühren sich diese beiden?“, und beide benutzen
|
|
`overlapping`, die Funktion, die du in Lektion neun eingetippt hast. Die eine steht in
|
|
`updateRocks`, wo ein Felsen auf das Schiff trifft:
|
|
|
|
[source,lua]
|
|
----
|
|
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
Die andere steht in `updateShots`, wo ein Schuss auf einen Felsen trifft:
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
Alles in dieser Lektion ändert diese beiden Zeilen und fügt ein paar kleine Funktionen hinzu, die
|
|
sie füttern. Sonst ändert sich am Spiel nichts. Das ganze fertige Skript liegt in
|
|
`learn/11-hitting-things.singe`, falls du dich verläufst.
|
|
|
|
[#_read_it_once_more]
|
|
=== Lies es noch einmal
|
|
|
|
Diesen Teil hat Lektion neun schon erledigt: `overlapping` ist Rechteckkollision, von Hand
|
|
ausgeschrieben. Lies es noch einmal, bevor du es wegwirfst, denn es ist die ganze Idee:
|
|
|
|
[source,lua]
|
|
----
|
|
function overlapping(ax, ay, aw, ah, bx, by, bw, bh)
|
|
if ax + aw <= bx then
|
|
return false
|
|
end
|
|
if bx + bw <= ax then
|
|
return false
|
|
end
|
|
if ay + ah <= by then
|
|
return false
|
|
end
|
|
if by + bh <= ay then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
----
|
|
|
|
Direkt ist die Frage „berühren sich diese beiden?“ schwer zu beantworten, rückwärts ist sie
|
|
leicht. Zwei Rechtecke berühren sich *nicht*, wenn eins von ihnen ganz links vom anderen liegt,
|
|
oder ganz rechts, oder ganz darüber, oder ganz darunter. Eine fünfte Art, sich zu verfehlen, gibt
|
|
es nicht. Wenn keine dieser vier stimmt, berühren sie sich.
|
|
|
|
Jedes `if` ist eine der vier Arten, sich zu verfehlen. `ax + aw` ist der rechte Rand des ersten
|
|
Rechtecks, also liest sich `ax + aw <= bx` als „das erste hört auf, bevor das zweite anfängt“, und
|
|
genau das bedeutet „ganz links“ in Zahlen. Dieselbe Form noch dreimal, für rechts, darüber und
|
|
darunter, mit `y` und den Höhen statt `x` und den Breiten, und dann bleibt nur noch `true` übrig.
|
|
|
|
Das sind die acht Zahlen, die jeder Kollisionstest braucht: eine Ecke und eine Größe für jedes der
|
|
beiden Dinge. Behalt die Form im Kopf, denn gleich löschst du sie.
|
|
|
|
[#_the_engine_already_has_it]
|
|
=== Die Engine hat es schon
|
|
|
|
`collideRects` kennst du aus Lektion sieben, wo es in Dodge die fallenden Blöcke erwischt hat.
|
|
Lektion neun hat denselben Test von Hand ausgeschrieben, damit du hineinsehen kannst. Du hast
|
|
hineingesehen. Lösch `overlapping` und ändere die beiden Zeilen aus *Wo du stehst* so, dass sie
|
|
stattdessen die Engine aufrufen:
|
|
|
|
[source,lua]
|
|
----
|
|
if collideRects(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and collideRects(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
|
|
----
|
|
|
|
Dieselben acht Zahlen, in derselben Reihenfolge: Ecke und Größe des ersten Rechtecks, dann die des
|
|
zweiten. Starte es, und das Spiel spielt sich genau wie vorher, und genau darum geht es. Der
|
|
einzige Unterschied liegt ganz am Rand. Dein `<=` hat gesagt, dass sich zwei Rechtecke, die sich
|
|
an einer Kante treffen, nicht berühren. `collideRects` sagt, dass sie es tun, und ein Rechteck ohne
|
|
Breite oder Höhe berührt überhaupt nichts. In solchen kniffligen Fällen gehen selbst geschriebene
|
|
Versionen still und leise schief, und sie sind ein Grund, die der Engine zu benutzen.
|
|
|
|
`collideRects` gehört zu einer Familie von Kollisionstests, und die Familie ist ein Teil dessen,
|
|
was dieses Buch das *Spiele-Kit* nennt: eine Handvoll schlichter Funktionen für die Dinge, die
|
|
früher fast jedes Spiel für sich selbst geschrieben hat. Zu prüfen, ob sich zwei Dinge berühren,
|
|
ist eins davon. Eine Zahl fürs nächste Mal aufzuheben ist ein anderes, und das ist Lektion zwölf.
|
|
Eine Weile warten, eine Zahl sanft von einem Wert zum anderen gleiten lassen und zeigen, wie lange
|
|
jeder Frame gedauert hat, sind der Rest. Die Referenz gibt jedem Teil einen eigenen Abschnitt:
|
|
Kollision, Spielstand, Timer, Tween und Statistik.
|
|
|
|
Die Kollisionstests sind die schlichtesten von allen: schlichte Fragen, ohne Handles, die man
|
|
aufheben muss, und ohne etwas, das man einrichten oder freigeben muss. Und keiner davon bewegt
|
|
irgendetwas. `collideRects` sagt dir, dass Schiff und Felsen am selben Ort sind; was du dagegen
|
|
tust, entscheidest du, und alles Interessante an einem Spiel passiert in dieser Entscheidung.
|
|
|
|
[#_the_picture_is_not_the_hit_box]
|
|
=== Das Bild ist nicht die Hitbox
|
|
|
|
Lektion zehn hat versprochen zu erklären, warum das Rechteck um einen runden Felsen die falsche
|
|
Form hat. Das Rechteck um das Schiff ist auch falsch, und zwar aus demselben Grund.
|
|
|
|
Zeichne das Rechteck des Schiffs, 32 mal 24, auf Papier und mal das Schiff darin aus. Es ist ein
|
|
Dreieck mit zwei Flossen, also ist in beiden oberen Ecken und zwischen den Flossen leerer Himmel.
|
|
Der Felsen ist rund, also ist in allen vier Ecken seines Quadrats leerer Himmel. Ein Felsen, der
|
|
die obere Ecke des Schiffsrechtecks streift, hat das Schiff nicht getroffen. Er ist am Flügel
|
|
vorbeigeflogen, und ein Spieler, der dafür ein Leben verliert, wird sagen, dass das Spiel
|
|
geschummelt hat -- und er hat recht.
|
|
|
|
Die Box, die du prüfst, ist also nicht das Bild, das du zeichnest. Sie ist kleiner, und sie sitzt
|
|
im Bild. Füg den Konstanten oben in deiner Datei drei Zahlen hinzu:
|
|
|
|
[source,lua]
|
|
----
|
|
local SHIP_PAD_X = 6
|
|
local SHIP_PAD_Y = 5
|
|
local ROCK_PAD = 4
|
|
----
|
|
|
|
Der Abstand sagt, wie weit vom jeweiligen Rand nach innen das echte Ding anfängt. Sechs Pixel weg
|
|
an jeder Seite des Schiffs und fünf oben und unten lassen eine Box von 20 mal 14 in der Mitte eines
|
|
Bilds von 32 mal 24 übrig, und da ist ungefähr der Rumpf. Vier weg an jeder Seite des Felsens
|
|
lassen 16 mal 16 übrig, und das ist fast nur Felsen.
|
|
|
|
Beide Boxen sind ein bisschen kleiner als die Dinge, für die sie stehen, und das ist Absicht. Eine
|
|
Box, die etwas zu klein ist, beschert dem Spieler Beinahe-Treffer, die sich wie Können anfühlen.
|
|
Eine Box, die etwas zu groß ist, beschert ihm Treffer, die sich wie Schummelei anfühlen. Spieler
|
|
verzeihen das Erste und nie das Zweite.
|
|
|
|
Statt diese Rechnung bei jedem Test auszuschreiben, schreib sie einmal pro Art von Ding:
|
|
|
|
[source,lua]
|
|
----
|
|
function rockBox(rock)
|
|
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
|
|
end
|
|
|
|
|
|
function shipBox()
|
|
return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, shipWidth - SHIP_PAD_X * 2, shipHeight - SHIP_PAD_Y * 2
|
|
end
|
|
----
|
|
|
|
Die Größen kommen aus `rockWidth`, `shipWidth` und ihren Freunden, den Messungen, die Lektion neun
|
|
an den Bildern vorgenommen hat, also steht im Spielcode immer noch keine einzige nackte
|
|
Pixelgröße. Zeichne das Schiff größer, und die Box wächst mit.
|
|
|
|
Jede dieser Funktionen gibt vier Zahlen auf einmal zurück, durch Kommas getrennt. Eine Funktion
|
|
darf so viele Werte zurückgeben, wie sie will, und auffangen kannst du sie mit einer Liste von
|
|
Namen links vom `=`:
|
|
|
|
[source,lua]
|
|
----
|
|
function shipHitsRock(rock)
|
|
local sx, sy, sw, sh = shipBox()
|
|
local rx, ry, rw, rh = rockBox(rock)
|
|
|
|
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
|
|
end
|
|
----
|
|
|
|
Jetzt liest sich der Test in `updateRocks` wie die Frage, die er stellt:
|
|
|
|
[source,lua]
|
|
----
|
|
if shipHitsRock(rock) then
|
|
----
|
|
|
|
Die Rechnung mit dem Abstand steht an genau zwei Stellen, und die Zahlen, die sie benutzt, stehen
|
|
oben in der Datei, also kannst du einstellen, wie fair sich das Spiel anfühlt, indem du eine Zahl
|
|
änderst, statt deine Tests zu durchsuchen.
|
|
|
|
Setz die neuen Funktionen an ihre Plätze im Alphabet, wie Lektion neun es verlangt hat: `rockBox`
|
|
nach `onSoundCompleted`, dann `shipBox` und `shipHitsRock`.
|
|
|
|
[#_firing_back]
|
|
=== Zurückschießen
|
|
|
|
Damit ist die Seite des Schiffs erledigt. Die andere Kollision im Spiel ist die, mit der du
|
|
zurückschießt: jeder Schuss gegen jeden Felsen, in `updateShots`. Sie hat dasselbe Problem wie das
|
|
Schiff, nur in klein. Ein Schuss ist ein Rechteck von 4 mal 10, das gegen das ganze Quadrat des
|
|
Felsens von 24 mal 24 geprüft wird, also zerbricht ein Schuss, der nur durch die leere Ecke neben
|
|
einem Felsen fliegt, ihn trotzdem.
|
|
|
|
Du könntest dem Schuss eine eigene Box mit Abstand geben. Sieh dir aber zuerst die Zahlen an. Zieh
|
|
von einem Schuss, der vier Pixel breit ist, einen Abstand ab, und es bleibt fast nichts übrig, und
|
|
ein ganzes Rechteck zu prüfen, nur um herauszufinden, ob ein Lichtsplitter etwas berührt hat, ist
|
|
mehr Maschinerie, als die Frage verdient.
|
|
|
|
[#_a_shot_is_almost_a_point]
|
|
=== Ein Schuss ist fast ein Punkt
|
|
|
|
Prüf stattdessen die Spitze. `collidePointRect` fragt, ob ein Punkt in einem Rechteck liegt --
|
|
zwei Zahlen für den Punkt, dann Ecke und Größe des Rechtecks:
|
|
|
|
[source,lua]
|
|
----
|
|
function shotHitsRock(shot, rock)
|
|
local rx, ry, rw, rh = rockBox(rock)
|
|
|
|
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
|
|
end
|
|
----
|
|
|
|
`shot.y` ist die Oberkante des Schusses, und `shot.x + shotWidth / 2` liegt auf seiner halben
|
|
Breite, also ist der geprüfte Punkt die Mitte der Nase des Schusses. Dort glaubt ein Spieler, dass
|
|
ein Schuss ist. Auf der Seite des Felsens steht dasselbe `rockBox`, das auch der Test des Schiffs
|
|
benutzt, also ist ein Felsen gleich groß, egal was ihn trifft.
|
|
|
|
Die Funktion kommt nach `shipHitsRock`, und der Test in `updateShots` wird zu:
|
|
|
|
[source,lua]
|
|
----
|
|
if not gone and shotHitsRock(shot, rock) then
|
|
----
|
|
|
|
[#_deciding_what_happened]
|
|
=== Entscheiden, was passiert ist
|
|
|
|
Keiner der Kollisionstests tut etwas. Sie antworten, und dein Code entscheidet. Hier sind die
|
|
beiden Stellen, die entscheiden, mit ihren neuen Fragen darin. Zuerst `updateRocks`:
|
|
|
|
[source,lua]
|
|
----
|
|
if shipHitsRock(rock) then
|
|
local channel = soundPlay(boomClip)
|
|
|
|
newRock(rock)
|
|
lives = lives - 1
|
|
if lives <= 0 then
|
|
dying = true
|
|
deathChannel = channel
|
|
if channel < 0 then
|
|
endGame()
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Und die Schleife in `updateShots`:
|
|
|
|
[source,lua]
|
|
----
|
|
for _, rock in ipairs(rocks) do
|
|
if not gone and shotHitsRock(shot, rock) then
|
|
newRock(rock)
|
|
score = score + HIT_SCORE
|
|
gone = true
|
|
soundPlay(boomClip, 0, 45)
|
|
end
|
|
end
|
|
----
|
|
|
|
Alles unter den Fragen ist genau das, was Lektion zehn dort hinterlassen hat. Ein Felsen, der das
|
|
Schiff trifft, knallt, geht zurück nach oben und kostet ein Leben. Ein Felsen, der abgeschossen
|
|
wird, knallt, geht zurück nach oben und bringt Punkte. Der Schuss verschwindet auch, und das
|
|
`not gone` verhindert, dass ein Schuss zwei Felsen zerbricht. Ändere irgendetwas davon, und du
|
|
änderst das Spiel, ohne einen einzigen Kollisionstest anzufassen, und genau darum werden die
|
|
Fragen und die Antworten getrennt gehalten.
|
|
|
|
Starte es. Flieg dicht an einem Felsen vorbei und lass ihn eine Flosse streifen: Du behältst das
|
|
Leben. Schieß knapp neben einen Felsen, und der Schuss segelt vorbei. Schieß mitten in einen
|
|
hinein, und er zerbricht, wie immer.
|
|
|
|
[#_seeing_the_boxes]
|
|
=== Die Boxen sehen
|
|
|
|
Die Hitboxen sind unsichtbar, und damit ist auch ein Fehler darin unsichtbar. Zeichne sie, während
|
|
du arbeitest. Noch eine Konstante oben:
|
|
|
|
[source,lua]
|
|
----
|
|
local SHOW_BOXES = false
|
|
----
|
|
|
|
Zwei weitere Funktionen, und die kommen ganz an den Anfang des Alphabets:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawBox(x, y, width, height)
|
|
overlayBox(x, y, x + width - 1, y + height - 1)
|
|
end
|
|
|
|
|
|
function drawBoxes()
|
|
colorForeground(0, 255, 0)
|
|
drawBox(shipBox())
|
|
for _, rock in ipairs(rocks) do
|
|
drawBox(rockBox(rock))
|
|
end
|
|
end
|
|
----
|
|
|
|
Und in `onOverlayUpdate`, nachdem das Schiff gezeichnet ist und bevor die Punkte obendrauf kommen:
|
|
|
|
[source,lua]
|
|
----
|
|
spriteDraw(shipSprite, shipX, shipY)
|
|
if SHOW_BOXES then
|
|
drawBoxes()
|
|
end
|
|
drawHud()
|
|
----
|
|
|
|
Ändere `SHOW_BOXES` in `true`, und grüne Umrisse erscheinen über den Grafiken, wie im Bild oben
|
|
in dieser Lektion. Ändere es zurück, wenn du fertig bist. Ein Schalter wie dieser, ein Wort oben in
|
|
einer Datei, das ein Bild davon einschaltet, was das Programm glaubt, ist mehr wert als noch so
|
|
viel Anstarren.
|
|
|
|
[#_what_just_happened_7]
|
|
=== Was gerade passiert ist
|
|
|
|
[source,lua]
|
|
----
|
|
function rockBox(rock)
|
|
return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2
|
|
end
|
|
----
|
|
|
|
Vier Werte aus einem `return`, durch Kommas getrennt. Lua gibt gern so viele zurück, wie du willst,
|
|
und der Aufrufer entscheidet, wie viele er behält. Wenn so ein Aufruf das *Letzte* in den Klammern
|
|
eines anderen Aufrufs ist, werden alle vier weitergereicht, und darum funktioniert
|
|
`drawBox(rockBox(rock))` und gibt `drawBox` seine vier Argumente. Überall sonst in einer
|
|
Argumentliste überlebt nur der erste Wert. Diese Regel beißt genau einmal zu, und zwar am Ende
|
|
dieser Lektion.
|
|
|
|
[source,lua]
|
|
----
|
|
return collideRects(sx, sy, sw, sh, rx, ry, rw, rh)
|
|
----
|
|
|
|
`collideRects` will acht Zahlen: Ecke und Größe, Ecke und Größe. Es gibt `true` oder `false`
|
|
zurück und ändert nichts. Weil die Antwort mit `return` direkt wieder aus der Funktion herauskommt,
|
|
liest sich `shipHitsRock(rock)` überall, wo es benutzt wird, wie eine Frage.
|
|
|
|
[source,lua]
|
|
----
|
|
return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh)
|
|
----
|
|
|
|
`collidePointRect` will sechs: einen Punkt, dann ein Rechteck. Der Rand zählt als innen, also
|
|
trifft ein Schuss genau auf der Grenze. Benutz es immer dann, wenn eine Seite der Frage so klein
|
|
ist, dass ihre Größe keine Rolle spielt.
|
|
|
|
[source,lua]
|
|
----
|
|
colorForeground(0, 255, 0)
|
|
overlayBox(x, y, x + width - 1, y + height - 1)
|
|
----
|
|
|
|
Beide kennst du aus Lektion sieben. `overlayBox` nimmt zwei *Ecken*, nicht eine Ecke und eine
|
|
Größe -- die einzige Stelle in dieser Lektion, an der die Zahlen ihre Form ändern -- und das `- 1`
|
|
ist da, weil beide Ecken dazugehören. `colorForeground` ist ein Schalter, der umgelegt bleibt, also
|
|
kommt alles, was `drawBoxes` zeichnet, grün heraus. Sprites und `overlayPrint` kümmern sich nicht
|
|
darum, also sieht der Rest des Spiels aus wie vorher.
|
|
|
|
[#_the_rest_of_the_family]
|
|
=== Der Rest der Familie
|
|
|
|
`collideRects` und `collidePointRect` tragen die meisten 2D-Spiele. Die anderen sind für die
|
|
Formen da, die Rechtecke schlecht beschreiben, und jede ist dieselbe Art von schlichter Frage:
|
|
|
|
* `collideCircles` und `collidePointCircle`, für alles Runde. Ein Kreis ist ein Mittelpunkt und
|
|
ein Radius.
|
|
* `collideRectCircle`, für ein rundes Ding gegen ein eckiges.
|
|
* `collidePointPolygon`, für eine Form mit eigenen Ecken -- ein Drache, ein Kontinent, ein seltsam
|
|
geformter Knopf. Du gibst ihm eine flache Liste von Zahlen: `x`, `y`, `x`, `y`.
|
|
* `collideSegments`, für zwei Strecken, die sich kreuzen.
|
|
|
|
Ehrlich gesagt ist dein Felsen rund, und die Kreistests passen besser zu ihm als jede Box. Der
|
|
Abstand ist die billige Antwort, und für die meisten Spiele reicht er, darum kommt er zuerst. Die
|
|
genaue Antwort ist eins der Dinge, die du unten ausprobieren kannst.
|
|
|
|
`collideSegments` ist wichtiger, als es klingt. Dein Schuss bewegt sich sechs Pixel pro Frame, und
|
|
die Felsen haben sechzehn Pixel Hitbox, also kann ein Schuss nicht an einem vorbei, ohne in ihm
|
|
geprüft zu werden. Mach den Schuss schnell genug -- vierzig Pixel pro Frame -- und er springt von
|
|
unterhalb eines Felsens nach oberhalb davon, ist kein einziges Mal am selben Ort wie der Felsen
|
|
und trifft nie irgendetwas. Die Linie von dort, wo der Schuss war, bis dorthin, wo er jetzt ist, zu
|
|
ziehen und `collideSegments` zu fragen, ob diese Linie einen Rand kreuzt, ist das Heilmittel.
|
|
Langsame Dinge brauchen es nicht.
|
|
|
|
Es gibt einen Kollisions-Callback in Singe, `onCollision`, und der ist nicht dafür da. Er gehört
|
|
zur 3D-Physik-Engine: Körper mit Masse, die in drei Dimensionen fallen und voneinander abprallen
|
|
und dir sagen, wo sie sich berührt haben und wie heftig. Du lernst ihn in Lektion vierundzwanzig
|
|
kennen. Nichts auf dem Overlay meldet sich von selbst, und nichts muss das. Deine Felsen sind in
|
|
einer Liste, du weißt, wie viele es sind, und Fragen ist billig.
|
|
|
|
Und wenn du nicht nur wissen willst, *ob* sich zwei Dinge getroffen haben, sondern auch, wo das
|
|
bewegte landen soll -- an einer Wand entlanggleiten, statt mitten in ihr stehen zu bleiben -- dann
|
|
beantworten die Kollisionsaufrufe das mit Absicht nicht. Für genau diese Aufgabe liegt Singe eine
|
|
Bibliothek namens `bump` bei, und der Abschnitt Mitgelieferte Bibliotheken in der Referenz sagt,
|
|
wie du an sie herankommst.
|
|
|
|
[#_try_it_11]
|
|
=== Probier es aus
|
|
|
|
. *Nimm den Abstand raus.* Setz `ROCK_PAD` auf `0` und spiel eine Minute. Dann setz es auf `8`
|
|
und spiel noch einmal. Bei einem davon fühlt es sich an, als würde das Spiel dich anlügen.
|
|
. *Sieh dir an, was du prüfst.* Setz `SHOW_BOXES` auf `true` und beobachte die grünen Umrisse,
|
|
während ein Felsen am Schiff vorbeizieht. Jetzt setz `SHIP_PAD_X` auf `16`, spiel und achte
|
|
darauf, dass dich nichts mehr treffen kann. Finde anhand der Rechnung in `shipBox` heraus,
|
|
warum.
|
|
. *Mach den Schuss zu schnell.* Setz `SHOT_SPEED` auf `40`. Zähl, wie viele von zehn Felsen du
|
|
triffst. Rechne an den Zahlen aus, warum das passiert, bevor du es zurückänderst.
|
|
. *Nimm den ganzen Schuss.* Schreib `shotHitsRock` so um, dass es `collideRects` mit dem vollen
|
|
Rechteck des Schusses benutzt statt `collidePointRect` mit seiner Nase. Spiel beide und
|
|
entscheide, welche dir lieber ist; es gibt keine richtige Antwort, und das zu wissen lohnt sich.
|
|
. *Mach den Felsen rund.* Gib dem Felsen einen Kreis statt einer Box: Sein Mittelpunkt liegt
|
|
`rock.x + rockWidth / 2` nach rechts und `rock.y + rockHeight / 2` nach unten, und ein Radius
|
|
von `10` passt ungefähr. Nimm `collideRectCircle` für das Schiff und `collidePointCircle` für
|
|
den Schuss, und zeichne den Kreis in `drawBoxes` mit `overlayCircle`, das einen Mittelpunkt und
|
|
einen Radius nimmt.
|
|
|
|
[#_break_it_on_purpose_10]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Die beiden Boxen sind schon je vier Werte, also ist es verlockend, sie direkt hineinzustecken und
|
|
die acht lokalen Namen wegzulassen:
|
|
|
|
[source,lua]
|
|
----
|
|
function shipHitsRock(rock)
|
|
return collideRects(shipBox(), rockBox(rock))
|
|
end
|
|
----
|
|
|
|
Das sieht richtig aus und ist falsch. Starte es, und das Spiel hält an, bevor es überhaupt etwas
|
|
zeichnet:
|
|
|
|
----
|
|
186:collideRects: Expected 8 argument(s), got 5.
|
|
----
|
|
|
|
Die Zeile ist die, die du gerade getippt hast, deine ist also vielleicht nicht 186. Die Fünf ist
|
|
aber dieselbe. `rockBox(rock)` ist das Letzte in den Klammern, also sind alle vier seiner Werte
|
|
hineingegangen. `shipBox()` ist es nicht, also wurde es auf einen gekürzt. Eins plus vier ist
|
|
fünf, und `collideRects` rät die anderen drei nicht.
|
|
|
|
Dieser Fehler kommt von der Engine und nicht von Lua, darum hat er dieselbe Form wie die in
|
|
Lektion neun und zehn: Die Zahl vorne ist die Zeile, dann kommt der Name der Funktion, die sich
|
|
geweigert hat, dann die Beschwerde. Setz die acht lokalen Namen wieder ein.
|
|
|
|
[#_what_you_learned_11]
|
|
=== Was du gelernt hast
|
|
|
|
* Kollision ist Rechnen mit Rechtecken, und du kannst sie selbst schreiben.
|
|
* Zwei Rechtecke verfehlen sich auf genau vier Arten; wenn keine davon stimmt, treffen sie sich.
|
|
* `collideRects` nimmt eine Ecke und eine Größe für jedes von zwei Rechtecken und antwortet
|
|
`true` oder `false`. Ränder, die sich nur berühren, zählen für ihn als Berührung.
|
|
* Die Kollisionstests gehören zum Spiele-Kit, schlichte Funktionen, bei denen man nichts
|
|
einrichten muss. Spielstand, Timer, Tween und Statistik sind der Rest davon.
|
|
* `collidePointRect` fragt, ob ein Punkt in einem Rechteck liegt, und das ist die richtige Frage
|
|
für alles, was klein ist.
|
|
* Die Box, die du prüfst, ist kleiner als das Bild, das du zeichnest, sonst fühlt sich das Spiel
|
|
unfair an.
|
|
* Eine Funktion kann mehrere Werte zurückgeben, und nur der letzte Aufruf in einer Argumentliste
|
|
reicht sie alle weiter.
|
|
* Die Kollisionstests antworten nur. Was danach passiert, ist dein Code, und wenn du die beiden
|
|
getrennt hältst, kannst du jedes davon allein ändern.
|
|
* Wenn du deine Hitboxen auf den Bildschirm zeichnest, wird aus einem unsichtbaren Fehler ein
|
|
sichtbarer.
|
|
* `onCollision` ist für Körper in der 3D-Physik, nicht für Sprites auf dem Overlay.
|
|
|
|
[#_next_time_11]
|
|
=== Beim nächsten Mal
|
|
|
|
Das Spiel zählt deine Punkte und nimmt dir Leben, und sobald du es beendest, ist die beste
|
|
Punktzahl, die je jemand erreicht hat, weg. Es steckt mitten in der Action, sobald es geladen ist.
|
|
Und es merkt sich, was es gerade tut, mit zwei Wahr-oder-falsch-Variablen, `over` und `dying`, und
|
|
das ist genau die Falle, vor der dich Lektion sieben gewarnt hat. Lektion zwölf gibt ihm einen
|
|
Titelbildschirm, eine Variable statt dieser beiden und einen Highscore, der morgen noch da ist.
|