[#_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.