575 lines
23 KiB
Text
575 lines
23 KiB
Text
[#_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.
|