[#_lesson_17_light_guns] == Lektion 17: Lightguns image::learn/17-light-guns.png[Die fertige Lektion, 480] Du baust eine Schießbude. Ziele erscheinen über dem Video bei Frames, die du aussuchst, du zielst auf den Bildschirm und drückst einen Abzug, und die, die du triffst, bringen Punkte. Um das zu schreiben, brauchst du keine Lightgun. Du brauchst eine Maus, denn für Singe *ist* eine Lightgun eine Maus: Sie meldet eine Position in denselben Koordinaten, über dieselben Aufrufe, und ihr Abzug kommt als derselbe Schalter an wie eine Maustaste. Schreib das Spiel für die Maus auf deinem Schreibtisch, und es funktioniert auf einem Automaten mit einer festgeschraubten Lightgun, und nur andersherum ist es vernünftig. Eine Lightgun ist keine andere Art von Eingabe. Sie ist eine Maus, mit der jemand zielt. [#_what_you_are_pointing_at] === Worauf du zielst Fang genauso an wie in Lektion vierzehn, mit dem Video, das die Engine für dich entpackt hat: ---- Singe -R -v Singe/menuBackground.mkv gallery ---- Dieses Material ist ein lila Gitter und ein Sonnenuntergang. Es ist kein Banküberfall. Jede Frame-Nummer in dieser Lektion bezieht sich darauf, damit der Code an dem Tag läuft, an dem du ihn eintippst, und der ganze Clip ist 720 mal 480 groß und etwa 420 Frames lang, und bei ungefähr dreißig Frames pro Sekunde sind das vierzehn Sekunden. Stell dir besseres Material vor. Dem Code ist egal, was auf dem Bild ist. [#_match_the_overlay_to_the_video] === Pass das Overlay an das Video an Schreib das oben in `gallery.singe`, unter die Zeile mit `dofile`: [source,lua] ---- overlaySetResolution(discGetWidth(), discGetHeight()) ---- Ohne diesen Aufruf ist das Overlay in jeder Richtung *halb* so groß wie das Video -- hier 360 mal 240 -- und jede Koordinate, mit der du zeichnest, und jede Mausposition, die du bekommst, liegt in diesem kleineren Raum. Am kleinen Overlay ist nichts falsch, aber es ist eine Umrechnung mehr, die du im Kopf behalten musst, während du herausfindest, wo ein Schuss gelandet ist. Wenn das Overlay zum Video passt, ist ein Ziel bei `x = 80` achtzig Pixel weit im Bild, und ein Schuss bei `x = 80` ebenso. Der Eintrag zu `overlaySetResolution` in der Referenz erklärt, was das kostet. [#_where_the_gun_is_pointing] === Wohin die Lightgun zielt Das beantwortet ein einziger Aufruf: [source,lua] ---- local x, y = mouseGetPosition(0) ---- `mouseGetPosition` gibt zwei Zahlen zurück, und das hast du noch keine Funktion tun sehen, aber Lua hat damit überhaupt kein Problem: Eine Funktion darf so viele Werte zurückgeben, wie sie will, und du fängst sie auf, indem du genauso viele Namen links vom `=` aufschreibst. Die `0` sagt, nach *welchem* Zeigegerät du fragst, von null an gezählt. Mit einer Maus oder einer Lightgun ist die Antwort immer Gerät `0`. Die Zahlen sind in Overlay-Koordinaten, nicht in Desktop-Pixeln, und darum hat sich der Aufruf oben gelohnt. Die Position ist die letzte, die das Gerät gemeldet hat, also kannst du sie abfragen, wann du willst und so oft du willst. Frag sie in `onOverlayUpdate` ab und zeichne mit der Antwort. Es gibt noch einen anderen Weg zu denselben Zahlen. `onMouseMoved` ist ein Callback, wie `onOverlayUpdate`, und die Engine ruft ihn jedes Mal auf, wenn sich ein Zeiger bewegt, und gibt ihm die neue Position. Das ist die richtige Wahl, wenn du auf Bewegung *reagieren* willst. Für ein Fadenkreuz willst du das nicht: Du willst wissen, wo das Ding gerade jetzt ist, und `mouseGetPosition` sagt dir das, ohne dass du eine Kopie aufheben musst. [#_drawing_a_crosshair] === Ein Fadenkreuz zeichnen Die Engine zeichnet dir keins. Ein Fadenkreuz besteht aus vier Linien und einem Kreis: [source,lua] ---- local function drawCrosshair(x, y) colorForeground(255, 255, 255, 255) overlayCircle(x, y, 10) overlayLine(x - 18, y, x - 4, y) overlayLine(x + 4, y, x + 18, y) overlayLine(x, y - 18, x, y - 4) overlayLine(x, y + 4, x, y + 18) end ---- Die Lücken sind wichtiger als die Linien. Wenn die vier Speichen vor dem Kreis aufhören, bleibt in der Mitte ein Loch, durch das du das Ziel siehst, und darum geht es bei einem Fadenkreuz überhaupt, und genau das zeichnen Anfänger als Erstes zu. Eine Sache solltest du fragen, bevor du es zeichnest: [source,lua] ---- local drawAim = singeWantsCrosshairs() ---- Das gibt `false` zurück, wenn der Spieler Singe mit `--nocrosshair` gestartet hat. Manche Lightguns setzen einen eigenen Punkt auf den Bildschirm, und zwei Fadenkreuze ein paar Pixel auseinander sind schlimmer als keins. Die Antwort kann sich nicht ändern, während das Spiel läuft, also lies sie einmal oben aus und heb sie in einer Variablen auf. Den Mauszeiger des Systems musst du nicht verstecken, denn das hat Singe schon getan. Es fängt die Maus beim Start ein, und dadurch bleibt sie im Fenster, und der Pfeil des Desktops ist versteckt. Willst du ihn je zurückhaben, etwa für einen Level-Editor, dann gibt `mouseSetCaptured(false)` die Maus frei, und die Taste, die mit `INPUT_GRAB` belegt ist, in der mitgelieferten Steuerung `G`, schaltet dasselbe während des Spielens um. [#_the_trigger] === Der Abzug Ein Abzug ist ein Knopf, und Knöpfe kommen bei `onInputPressed` an, das du zum ersten Mal in Lektion drei gesehen hast: [source,lua] ---- function onInputPressed(what, device) if what ~= SWITCH_BUTTON3 then return end local x, y = mouseGetPosition(device or 0) -- ... end ---- `SWITCH_BUTTON3` ist die linke Maustaste, und bei einer Lightgun ist es der Abzug. Die mitgelieferte `controls.cfg` belegt die linke Taste mit `SWITCH_BUTTON3`, die rechte mit `SWITCH_BUTTON1` und die mittlere mit `SWITCH_BUTTON2`. Diese Nummern sehen durcheinandergewürfelt aus, und das sind sie auch: Die Schalter sind die Knöpfe des Arcade-Bedienfelds, und welches echte Bedienelement mit welchem verdrahtet ist, ist Sache des Automaten, nicht des Spiels. Die linke Umschalttaste ist ebenfalls mit `SWITCH_BUTTON3` belegt, also kannst du beim Testen auch ohne Maus schießen. Das zweite Argument ist das Neue. Wenn der Schalter von einer Maus oder einer Lightgun kam, ist `device` die Nummer dieses Geräts -- `0` für das erste, `1` für das zweite und so weiter. Wenn er von einer Taste oder einem Gamepad kam, ist es `nil`, denn dann war kein Zeigegerät beteiligt. Darum steht im Aufruf oben `device or 0`: Wenn `device` `nil` ist, nimm stattdessen `0`. Du wirst `or` ständig so benutzen, und es liest sich genau so, wie es klingt -- „device oder nichts, und dann null“. Dieses eine zusätzliche Argument macht einen Automaten mit zwei Lightguns möglich. Die Lightgun von Spieler eins ist Gerät `0`, die von Spieler zwei ist Gerät `1`, und dasselbe `onInputPressed` unterscheidet sie, ohne dass du raten musst. [#_did_it_hit_anything] === Hat er etwas getroffen? Wie man das beantwortet, weißt du schon. Das ist Lektion elf: [source,lua] ---- if collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then ---- Ein Schuss ist ein Punkt. Ein Ziel ist ein Rechteck. `collidePointRect` fragt, ob der Punkt im Rechteck liegt, und der Rand zählt als innen. Am Schießen ist nichts Besonderes. Der Treffertest eines Lightgun-Spiels ist derselbe Treffertest, mit dem ein Plattformspiel herausfindet, ob du auf einem Block gelandet bist, und die `collide`-Familie in der Referenz hat die übrigen Formen. [#_targets_that_come_and_go] === Ziele, die kommen und gehen Das Einzige, was Video ändert, ist, *wann* es ein Ziel gibt. In Lektion elf war ein Felsen da, bis du ihn abgeschossen hast. Hier ist ein Ziel auf dem Bildschirm, solange der Film den Teil der Szene zeigt, zu dem es gehört, und dann ist es weg, ob du es getroffen hast oder nicht. Das sind zwei Felder mehr im Datensatz, und das Werkzeug dafür hat dir Lektion sechs gebaut: [source,lua] ---- local targets = { { first = 40, last = 110, x = 80, y = 110 }, { first = 120, last = 190, x = 470, y = 140 }, { first = 200, last = 270, x = 290, y = 250 }, { first = 280, last = 360, x = 150, y = 300 }, } ---- Vier Datensätze, jeder mit dem Frame, in dem das Ziel erscheint, dem Frame, in dem es verschwindet, und der Stelle, an der es sitzt. Ein fünftes Ziel hinzuzufügen heißt, eine Zeile hinzuzufügen. Zu dieser Form solltest du jedes Mal greifen, wenn du gerade dabei bist, dieselben vier Zeilen Code mit anderen Zahlen darin zu schreiben. Ob ein Ziel zählt, ist dann eine kleine eigene Funktion: [source,lua] ---- local function targetIsUp(target, frame) return not target.hit and frame >= target.first and frame <= target.last end ---- Das Zeichnen fragt sie, und das Schießen auch, und genau dafür steht sie an einer Stelle: Ein Ziel, das du sehen kannst, und ein Ziel, das du treffen kannst, können sich nie widersprechen. [#_the_whole_thing_9] === Das Ganze [source,lua] ---- -- Programmieren lernen mit Singe -- Lektion 17: Lightguns dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local FIRST_FRAME = 20 local LAST_FRAME = 400 local TARGET_SIZE = 96 local HIT_SCORE = 100 local FLASH_TIME = 12 local targets = { { first = 40, last = 110, x = 80, y = 110 }, { first = 120, last = 190, x = 470, y = 140 }, { first = 200, last = 270, x = 290, y = 250 }, { first = 280, last = 360, x = 150, y = 300 }, } local aimX = overlayGetWidth() / 2 local aimY = overlayGetHeight() / 2 local score = 0 local shots = 0 local hits = 0 local flash = 0 local flashX = 0 local flashY = 0 local drawAim = singeWantsCrosshairs() local function drawCrosshair(x, y) colorForeground(255, 255, 255, 255) overlayCircle(x, y, 10) overlayLine(x - 18, y, x - 4, y) overlayLine(x + 4, y, x + 18, y) overlayLine(x, y - 18, x, y - 4) overlayLine(x, y + 4, x, y + 18) end local function targetIsUp(target, frame) return not target.hit and frame >= target.first and frame <= target.last end local function startRound() score = 0 shots = 0 hits = 0 flash = 0 for _, target in ipairs(targets) do target.hit = false end discSkipToFrame(FIRST_FRAME) end function onInputPressed(what, device) if what == SWITCH_START1 then startRound() return end if what ~= SWITCH_BUTTON3 then return end local x, y = mouseGetPosition(device or 0) local frame = discGetFrame() shots = shots + 1 for _, target in ipairs(targets) do if targetIsUp(target, frame) and collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then target.hit = true hits = hits + 1 score = score + HIT_SCORE flash = FLASH_TIME flashX = x flashY = y return end end end function onOverlayUpdate() local frame = discGetFrame() if frame >= LAST_FRAME then startRound() frame = FIRST_FRAME end aimX, aimY = mouseGetPosition(0) overlayClear() for _, target in ipairs(targets) do if targetIsUp(target, frame) then colorForeground(255, 200, 0, 255) overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1) overlayCircle(target.x + TARGET_SIZE / 2, target.y + TARGET_SIZE / 2, TARGET_SIZE / 3) end end if flash > 0 then colorForeground(255, 255, 255, 255) overlayCircle(flashX, flashY, 30 - flash * 2) flash = flash - 1 end overlayPrint(2, 1, "SCORE " .. score .. " HITS " .. hits .. "/" .. shots .. " FRAME " .. frame) if drawAim then drawCrosshair(aimX, aimY) end return OVERLAY_UPDATED end startRound() ---- Ziel auf ein gelbes Kästchen und klick. Der Zähler in der Ecke geht hoch, ein weißer Ring öffnet sich da, wo du getroffen hast, und das Kästchen verschwindet. Daneben geschossen, und nur die Zahl der Schüsse geht hoch, und das ist die billigste Punktetafel, die es gibt, und sie sagt einem Spieler mehr darüber, wie er sich schlägt, als die Punktzahl. Die Runde fängt von selbst neu an, wenn der Film zu Ende ist, und mit `1` fängt sie neu an, wann immer du willst. [#_what_just_happened_13] === Was gerade passiert ist [source,lua] ---- local frame = discGetFrame() if frame >= LAST_FRAME then startRound() frame = FIRST_FRAME end ---- Das Video ist die Uhr, wie schon in Lektion vierzehn, und mit `discGetFrame` liest du sie ab. Alles andere im Frame wird von dieser einen Zahl aus entschieden. Wenn der Film über das Ende des letzten Ziels hinausläuft, fängt die Runde von vorn an: `startRound` löscht die Punkte, markiert jedes Ziel als nicht getroffen und schickt die Disc mit `discSkipToFrame` zurück an den Anfang. Die Zeile darunter setzt auch die lokale Kopie von `frame`, denn die wurde vor dem Sprung gelesen und enthält noch die alte Zahl. Ohne sie würde das Zeichnen darunter einen Frame lang mit einer Zahl arbeiten, die nicht mehr stimmt. [source,lua] ---- shots = shots + 1 for _, target in ipairs(targets) do if targetIsUp(target, frame) and collidePointRect(...) then ---- Zähl den Schuss, bevor du nach einem Treffer suchst, damit auch ein Fehlschuss zählt. Dann geh die Liste durch. Das `return` in der Schleife hört beim ersten getroffenen Ziel auf, und das heißt, dass eine Kugel nicht zwei sich überlappende Ziele erwischen kann -- eine Regel, die du so oder so festlegen musst, und das hier ist die Zeile, die sie festlegt. [source,lua] ---- overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1) ---- `overlayBox` will zwei gegenüberliegende Ecken, nicht eine Ecke und eine Größe, also ist die ferne Ecke die nahe plus die Größe. Das `- 1` kommt daher, dass beide Ecken dazugehören: Ein Kästchen von `80` bis `175` ist sechsundneunzig Pixel breit. `collidePointRect` will dagegen eine Ecke und eine Größe, und darum steht `TARGET_SIZE` dort für sich allein und wird hier zur Ecke addiert. Die beiden zu verwechseln ist ein wirklich häufiger Bug, und er zeigt sich als Ziele, die du ein Stück außerhalb der Stelle treffen kannst, an der sie gezeichnet sind. [source,lua] ---- if flash > 0 then colorForeground(255, 255, 255, 255) overlayCircle(flashX, flashY, 30 - flash * 2) flash = flash - 1 end ---- `flash` ist ein Countdown in Frames, der auf zwölf gesetzt wird, wenn ein Schuss trifft, und jedes Mal um eins kleiner wird, wenn der Bildschirm gezeichnet wird. Weil der Radius *aus* dem Countdown ausgerechnet wird, wächst der Ring, während die Zahl schrumpft. Zwölf Frames sind weniger als eine halbe Sekunde. Eine Rückmeldung, die länger dauert, fühlt sich nicht mehr wie eine Folge des Abzugs an, sondern wie Wetter. [#_two_guns] === Zwei Lightguns Du bist zwei Änderungen von einem Automaten für zwei Spieler entfernt, und keine davon steckt in der Spielschleife. Die erste ist `mouseSetMode(MOUSE_MANY)`, das Singe sagt, jedes Gerät einzeln zu lesen, statt alle zu einem Mauszeiger zusammenzulegen. `mouseHowMany()` sagt, wie viele es beim Start gefunden hat, also fragt ein Spiel normalerweise nach, bevor es sich festlegt: [source,lua] ---- if mouseHowMany() >= 2 then mouseSetMode(MOUSE_MANY) end ---- Danach ist `mouseGetPosition(1)` die zweite Lightgun, und das Argument `device` von `onInputPressed` sagt, welche Lightgun geschossen hat. Behalte zwei Fadenkreuze, zwei Punktestände und eine Liste von Zielen, und du hast ein kooperatives Spiel. Die zweite Änderung steht gar nicht in deinem Skript. Die mitgelieferte `controls.cfg` belegt nur die Tasten der *ersten* Maus, also kommt der Abzug der zweiten Lightgun nirgends an. Ein Automat mit zwei Lightguns braucht in dieser Datei eine eigene Zeile, die beide nennt: [source,lua] ---- INPUT_ACTION_3 = { SCANCODE.LSHIFT, GAMEPAD_0.BUTTON_X, MOUSE_0.BUTTON_LEFT, MOUSE_1.BUTTON_LEFT } ---- Schreib das in deine Anleitung. Ein Spieler, dessen zweite Lightgun nichts tut, gibt deinem Spiel die Schuld, und damit hat er halb recht. [#_the_other_way_to_test_a_hit] === Die andere Art, einen Treffer zu prüfen Es gibt eine zweite Art, die Frage „Hat dieser Schuss etwas getroffen?“ zu beantworten, und es lohnt sich, von ihr zu wissen, auch wenn du sie heute nicht benutzt. `vldpGetPixel(x, y)` liest die Farbe eines Pixels des Video-Frames, der gerade auf dem Bildschirm ist. Spiele benutzen das seit der allerersten Version von Singe: Mal jedes Ziel im Material in einer Farbe an, die sonst nirgends vorkommt, und frag beim Abdrücken, welche Farbe unter dem Fadenkreuz ist. Das bringt dir Trefferformen, die der Handlung genau folgen, umsonst und ohne Rechtecke, die du anlegen musst. Es kostet dich Material, das du dafür vorbereiten musst, und einen Treffertest, den du weder sehen noch debuggen kannst. Der Eintrag zu `vldpGetPixel` in der Referenz hat die Einzelheiten. Rechtecke sind die richtige Voreinstellung; das hier behältst du in der Hinterhand. [#_what_only_a_cabinet_cares_about] === Was nur einen Automaten kümmert Alles oben funktioniert auf deinem Schreibtisch. Einige Dinge gibt es auf deinem Schreibtisch überhaupt nicht, und jetzt ist ein guter Moment, sie zu nennen, damit sie dich später nicht überraschen. *Kalibrierung.* Einer echten Lightgun muss man sagen, wo der Bildschirm ist, denn sie ist eine Kamera, die von da, wo der Spieler gerade steht, auf einen Monitor schaut. Die Service-Tools von Singe haben einen Bildschirm Light Gun, der Ziele in die Mitte und in jede Ecke setzt und markiert, wohin die Lightgun tatsächlich gezielt hat, also siehst du, in welche Richtung sie danebenliegt. Dein Spiel kalibriert nicht und sollte es auch nicht versuchen. *Der Sinden-Rahmen.* Eine beliebte Familie von Lightguns findet den Bildschirm, indem sie nach einem hellen Rahmen sucht, der um das Bild gezeichnet ist. `--sindengun` zeichnet diesen Rahmen, und die Engine rechnet die Positionen zurück in das kleinere Bild darin, also bekommt dein Spiel weiterhin Koordinaten, die das bedeuten, was sie vorher bedeutet haben. *Bildschirme, die nicht die Form des Films haben.* `ratioGetX()` und `ratioGetY()` geben dir die Zahlen, die ein Spieler auf der Befehlszeile angegeben hat, für einen Bildschirm, dessen Proportionen nicht zum Material passen. Singe wendet sie nicht an; ein Spiel, das sie haben will, rechnet selbst. *Lightguns, die verschwinden.* `SWITCH_MOUSE_DISCONNECT` kommt bei `onInputPressed` an, wenn eine Lightgun ausgesteckt wird. Kein Knopf erzeugt es; die Engine löst es aus. Ein Spiel, das darauf achtet, kann „PLAYER 2 GUN DISCONNECTED“ auf den Bildschirm schreiben, statt so auszusehen, als sei es abgestürzt. *Gar keine Maus.* Ein Automat mit einem Joystick und ohne Zeigegerät kann den Mauszeiger mit dem Stick steuern, und das schaltet `joyMouseEnable` ein. Nichts davon ändert das Spiel, das du geschrieben hast. Das alles ist Lektion einunddreißig. [#_try_it_17] === Probier es aus . *Verschieb ein Ziel.* Ändere das `x` des dritten Ziels in `680` und starte es. Der größte Teil davon hängt jetzt über den rechten Rand hinaus, denn das Overlay ist 720 breit und das Ziel 96. Den schmalen Streifen, der übrig ist, kannst du immer noch treffen. Entscheide, was an den Rändern passieren soll. . *Mach die Ziele leichter.* Ändere `TARGET_SIZE` in `140`. Achte darauf, dass sich das Zeichnen und der Treffertest beide ändern und dass du nur eine Zahl geändert hast. Dafür ist eine benannte Konstante da. . *Zähl die Treffsicherheit.* Füg eine Zeile hinzu, die `hits` als Prozentsatz von `shots` ausgibt. Sichere dich gegen das Teilen durch null vor dem ersten Schuss ab und finde heraus, was Lua ausgibt, wenn du es nicht tust. . *Lass ein Ziel dich bestrafen.* Füg einem Datensatz ein Feld `penalty = true` hinzu und zieh Punkte ab, wenn es getroffen wird. Achte darauf, dass du einem Datensatz ein Feld hinzufügst, nicht allen vieren, und dass die ohne das Feld dort `nil` haben. . *Nimm das `return` heraus* aus der Schleife in `onInputPressed` und lass dann zwei Ziele sich überlappen, indem du ihnen dieselben Frames und fast dieselbe Position gibst. Ein Schuss erwischt jetzt beide. Entscheide, welches Verhalten dein Spiel will. [#_break_it_on_purpose_16] === Mach es absichtlich kaputt Ändere den Schusstest so, dass er `SWITCH_BUTTON1` statt `SWITCH_BUTTON3` benutzt, und starte es. Nichts geht kaputt. Kein Fehler erscheint. Die linke Taste tut nichts mehr, und jetzt schießt die *rechte*. Das ist die schlimmste Art von Bug und die häufigste in Eingabecode: Das Programm ist nicht falsch, es ist nur mit dem falschen Ding verdrahtet. Es gibt keine Fehlermeldung zu lesen, denn du hast eine vernünftige Frage zu einem Schalter gestellt, den es gibt, und er hat ehrlich geantwortet. Zwei Dinge finden ihn. Das erste ist `debugPrint` aus Lektion acht: [source,lua] ---- function onInputPressed(what, device) debugPrint("switch " .. what .. " from device " .. tostring(device)) ---- Jetzt gibt jeder Druck eine Zeile aus, und du siehst, was der Abzug wirklich schickt. Beachte `tostring(device)`: `device` ist bei einem Tastendruck `nil`, und `nil` mit `..` an einen String zu hängen ist ein Fehler. `tostring` macht daraus das Wort `nil`, und die Zeile wird ausgegeben. Das zweite ist der eigene Input Test der Engine in den Service-Tools, der jedes Gerät zeigt, das sie sieht, und den letzten Schalter, den das Spiel bekommen hätte. Wenn ein Knopf nichts tut, sagt dir dieser Bildschirm, welche Hälfte des Problems du hast: Entweder kommt der Schalter an und dein Spiel ignoriert ihn, oder er kommt gar nicht an. [#_what_you_learned_17] === Was du gelernt hast * Eine Lightgun meldet sich als Maus, also funktioniert ein Spiel, das für die Maus geschrieben ist, mit einer Lightgun. * `mouseGetPosition(device)` gibt zwei Werte zurück, die Position des Zeigers in Overlay-Koordinaten. * Eine Funktion in Lua kann mehr als einen Wert zurückgeben, und du fängst sie auf, indem du genauso viele Namen aufschreibst. * `singeWantsCrosshairs()` sagt, ob du dein eigenes Fadenkreuz zeichnen sollst. Lies es einmal aus. * Singe fängt den Mauszeiger des Systems beim Start ein und versteckt ihn; das musst du nicht tun. * `onInputPressed` bekommt ein zweites Argument, das nennt, welche Maus oder Lightgun geschossen hat, und `nil`, wenn es eine Taste oder ein Gamepad war. * Ein Treffertest gegen ein Rechteck ist `collidePointRect`, dieselbe Kollision, die du in Lektion elf gelernt hast. * Das Frame-Fenster eines Ziels ist nur zwei Felder mehr in seinem Datensatz. * `a or b` gibt dir `b`, wenn `a` `nil` ist. * Kalibrierung, der Sinden-Rahmen und die Verdrahtung für zwei Lightguns liegen außerhalb deines Skripts. [#_next_time_17] === Beim nächsten Mal Deine Schießbude verlangt eine Sache: triff das, dort. Die andere Arcade-Konvention, auf der Videospiele aufbauen, verlangt etwas Schwierigeres -- drück *das*, *jetzt*, und du hast eine halbe Sekunde. Lektion achtzehn baut ein Quick-Time-Event, mit einer Aufforderung, einem Zeitfenster, das in Frames gemessen wird, und drei verschiedenen Enden, je nachdem, ob du richtig, falsch oder zu langsam warst.