596 lines
23 KiB
Text
596 lines
23 KiB
Text
[#_lesson_17_light_guns]
|
||
== Leçon 17 : Les pistolets optiques
|
||
|
||
image::learn/17-light-guns.png[La leçon terminée, 480]
|
||
|
||
Vous allez faire un stand de tir. Des cibles apparaissent par-dessus la vidéo
|
||
aux images que vous choisissez, vous visez l'écran et vous appuyez sur une
|
||
gâchette, et celles que vous touchez rapportent des points.
|
||
|
||
Vous n'avez pas besoin d'un pistolet optique pour écrire ceci. Il vous faut une
|
||
souris, parce que pour Singe, un pistolet optique *est* une souris : il signale
|
||
une position dans les mêmes coordonnées, par les mêmes appels, et sa gâchette
|
||
arrive comme le même contact qu'un bouton de souris. Écrivez le jeu pour la
|
||
souris qui est sur votre bureau et il marche sur une borne où un pistolet est
|
||
boulonné, et c'est la seule façon raisonnable de s'y prendre. Un pistolet n'est
|
||
pas une autre sorte d'entrée. C'est une souris que quelqu'un a pointée.
|
||
|
||
[#_what_you_are_pointing_at]
|
||
=== Ce que vous visez
|
||
|
||
Commencez comme à la leçon quatorze, avec la vidéo que le moteur a déballée pour
|
||
vous :
|
||
|
||
----
|
||
Singe -R -v Singe/menuBackground.mkv gallery
|
||
----
|
||
|
||
Ces images, c'est une grille violette et un coucher de soleil. Ce n'est pas un
|
||
braquage de banque. Chaque numéro d'image de cette leçon s'y rapporte, pour que
|
||
le code tourne le jour même où vous le tapez, et le clip entier fait 720 sur 480
|
||
et environ 420 images, ce qui, à une trentaine d'images par seconde, fait
|
||
quatorze secondes. Imaginez de meilleures images. Le code se moque de ce qu'il y
|
||
a dans l'image.
|
||
|
||
[#_match_the_overlay_to_the_video]
|
||
=== Accordez l'overlay à la vidéo
|
||
|
||
Mettez ceci en haut de `gallery.singe`, sous la ligne `dofile` :
|
||
|
||
[source,lua]
|
||
----
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
----
|
||
|
||
Sans cet appel, l'overlay fait la *moitié* de la vidéo dans chaque direction --
|
||
360 sur 240 ici -- et chaque coordonnée avec laquelle vous dessinez, et chaque
|
||
position de souris qu'on vous donne, est dans cet espace plus petit. Le petit
|
||
overlay n'a rien de mal, mais c'est une conversion de plus à garder en tête
|
||
pendant que vous cherchez où un tir est tombé. Accorder l'overlay à la vidéo
|
||
veut dire qu'une cible à `x = 80` est à quatre-vingts pixels du bord gauche de
|
||
l'image, et un tir à `x = 80` aussi. L'entrée de la Référence consacrée à
|
||
`overlaySetResolution` explique ce que ça coûte.
|
||
|
||
[#_where_the_gun_is_pointing]
|
||
=== Où vise le pistolet
|
||
|
||
Un seul appel y répond :
|
||
|
||
[source,lua]
|
||
----
|
||
local x, y = mouseGetPosition(0)
|
||
----
|
||
|
||
`mouseGetPosition` renvoie deux nombres, ce que vous n'avez encore jamais vu
|
||
une fonction faire et qui ne pose aucun problème à Lua : une fonction peut
|
||
renvoyer autant de valeurs qu'elle veut, et vous les attrapez en écrivant autant
|
||
de noms à gauche du `=`. Le `0` indique *quel* appareil de pointage vous
|
||
interrogez, en comptant à partir de zéro. Avec une seule souris, ou un seul
|
||
pistolet, la réponse est toujours l'appareil `0`.
|
||
|
||
Les nombres sont en coordonnées de l'overlay, pas en pixels du bureau, et c'est
|
||
pourquoi l'appel ci-dessus valait la peine. La position est la dernière que
|
||
l'appareil a signalée, donc vous pouvez la demander quand vous voulez, aussi
|
||
souvent que vous voulez. Demandez-la dans `onOverlayUpdate` et dessinez avec la
|
||
réponse.
|
||
|
||
Il y a une autre façon d'obtenir les mêmes nombres. `onMouseMoved` est un
|
||
callback, comme `onOverlayUpdate`, et le moteur l'appelle chaque fois qu'un
|
||
pointeur bouge, en lui donnant la nouvelle position. C'est le bon choix quand
|
||
vous voulez *réagir* au mouvement. Pour dessiner un réticule, ce n'est pas le
|
||
cas : vous voulez savoir où se trouve la chose en ce moment, et
|
||
`mouseGetPosition` vous le dit sans que vous ayez à en garder une copie.
|
||
|
||
[#_drawing_a_crosshair]
|
||
=== Dessiner un réticule
|
||
|
||
Le moteur n'en dessinera pas pour vous. Un réticule, c'est quatre traits et un
|
||
cercle :
|
||
|
||
[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
|
||
----
|
||
|
||
Les vides comptent plus que les traits. Arrêter les quatre branches avant le
|
||
cercle vous laisse un trou au milieu pour voir la cible à travers, ce qui est
|
||
tout l'intérêt d'un réticule et la première chose que les débutants recouvrent.
|
||
|
||
Il y a une chose à demander avant de le dessiner :
|
||
|
||
[source,lua]
|
||
----
|
||
local drawAim = singeWantsCrosshairs()
|
||
----
|
||
|
||
Cet appel renvoie `false` quand le joueur a lancé Singe avec `--nocrosshair`.
|
||
Certains pistolets optiques affichent leur propre point à l'écran, et deux
|
||
réticules à quelques pixels l'un de l'autre, c'est pire qu'aucun. La réponse ne
|
||
peut pas changer pendant que le jeu tourne, alors lisez-la une fois en haut et
|
||
gardez-la dans une variable.
|
||
|
||
Vous n'avez pas à cacher le pointeur du système, parce que Singe l'a déjà fait.
|
||
Il capture la souris au démarrage, ce qui la confine à la fenêtre et cache la
|
||
flèche du bureau. Si vous voulez un jour la récupérer -- pour un éditeur de
|
||
niveaux, par exemple -- `mouseSetCaptured(false)` la libère, et la touche
|
||
affectée à `INPUT_GRAB`, qui est `G` dans les commandes fournies, bascule la
|
||
même chose pendant que vous jouez.
|
||
|
||
[#_the_trigger]
|
||
=== La gâchette
|
||
|
||
Une gâchette est un bouton, et les boutons arrivent dans `onInputPressed`, que
|
||
vous avez rencontré pour la première fois à la leçon trois :
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what, device)
|
||
if what ~= SWITCH_BUTTON3 then
|
||
return
|
||
end
|
||
|
||
local x, y = mouseGetPosition(device or 0)
|
||
-- ...
|
||
end
|
||
----
|
||
|
||
`SWITCH_BUTTON3` est le bouton gauche de la souris, et sur un pistolet, c'est la
|
||
gâchette. Le `controls.cfg` fourni affecte le bouton gauche à `SWITCH_BUTTON3`,
|
||
le droit à `SWITCH_BUTTON1`, et celui du milieu à `SWITCH_BUTTON2`. Ces numéros
|
||
ont l'air mélangés parce qu'ils le sont : les contacts sont les boutons du
|
||
panneau d'arcade, et savoir quelle commande physique est câblée à quel contact,
|
||
c'est l'affaire de la borne, pas du jeu. Maj gauche est aussi affectée à
|
||
`SWITCH_BUTTON3`, pour que vous puissiez tirer sans souris pendant vos tests.
|
||
|
||
Le second argument est la partie nouvelle. Quand le contact vient d'une souris
|
||
ou d'un pistolet optique, `device` est l'indice de cet appareil -- `0` pour le
|
||
premier, `1` pour le second, et ainsi de suite. Quand il vient d'une touche ou
|
||
d'une manette, il vaut `nil`, parce qu'aucun appareil de pointage n'était en
|
||
jeu. C'est pourquoi l'appel ci-dessus dit `device or 0` : si `device` vaut
|
||
`nil`, utilisez `0` à la place. Vous utiliserez `or` comme ça sans arrêt, et ça
|
||
se lit exactement comme ça sonne en anglais -- « device, ou rien, auquel cas
|
||
zéro ».
|
||
|
||
C'est ce seul argument de plus qui rend possible une borne à deux pistolets. Le
|
||
pistolet du joueur un est l'appareil `0` et celui du joueur deux est l'appareil
|
||
`1`, et le même `onInputPressed` les distingue sans que vous ayez à deviner.
|
||
|
||
[#_did_it_hit_anything]
|
||
=== Est-ce que ça a touché quelque chose
|
||
|
||
Vous savez déjà répondre à ça. C'est la leçon onze :
|
||
|
||
[source,lua]
|
||
----
|
||
if collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then
|
||
----
|
||
|
||
Un tir est un point. Une cible est un rectangle. `collidePointRect` demande si
|
||
le point est à l'intérieur du rectangle, et le bord compte comme l'intérieur.
|
||
Tirer n'a rien de spécial. Le test d'impact d'un jeu de tir est le même que
|
||
celui qu'utilise un jeu de plateforme pour savoir si vous avez atterri sur un
|
||
bloc, et la famille `collide` de la Référence a le reste des formes.
|
||
|
||
[#_targets_that_come_and_go]
|
||
=== Des cibles qui vont et viennent
|
||
|
||
La seule chose que la vidéo change, c'est *quand* une cible existe. À la leçon
|
||
onze, un rocher restait là jusqu'à ce que vous tiriez dessus. Ici, une cible est
|
||
à l'écran pendant que le film montre la partie de la scène à laquelle elle
|
||
appartient, et ensuite elle disparaît, que vous l'ayez touchée ou non.
|
||
|
||
Ce sont deux champs de plus dans l'enregistrement, et la leçon six vous a
|
||
fabriqué l'outil :
|
||
|
||
[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 },
|
||
}
|
||
----
|
||
|
||
Quatre enregistrements, chacun avec l'image où la cible apparaît, l'image où
|
||
elle s'en va, et l'endroit où elle se trouve. Ajouter une cinquième cible, c'est
|
||
ajouter une ligne. C'est la forme à adopter chaque fois que vous vous surprenez
|
||
sur le point d'écrire les quatre mêmes lignes de code avec des nombres
|
||
différents dedans.
|
||
|
||
Savoir si une cible compte est alors une petite fonction à part entière :
|
||
|
||
[source,lua]
|
||
----
|
||
local function targetIsUp(target, frame)
|
||
return not target.hit and frame >= target.first and frame <= target.last
|
||
end
|
||
----
|
||
|
||
Le dessin lui pose la question, et le tir aussi, et c'est tout l'intérêt de la
|
||
mettre à un seul endroit : une cible que vous voyez et une cible que vous pouvez
|
||
toucher ne peuvent jamais être en désaccord.
|
||
|
||
[#_the_whole_thing_9]
|
||
=== Le tout
|
||
|
||
[source,lua]
|
||
----
|
||
-- Apprendre à programmer avec Singe -- Leçon 17 : Les pistolets optiques
|
||
|
||
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()
|
||
----
|
||
|
||
Visez une boîte jaune et cliquez. Le compteur dans le coin augmente, un anneau
|
||
blanc s'ouvre là où vous avez touché, et la boîte disparaît. Ratez, et seul le
|
||
compteur de tirs augmente, ce qui est l'afficheur de scores le moins cher qui
|
||
soit et en dit plus à un joueur sur sa forme que le score. La manche recommence
|
||
d'elle-même quand le film arrive au bout, et `1` la relance quand vous voulez.
|
||
|
||
[#_what_just_happened_13]
|
||
=== Ce qui vient de se passer
|
||
|
||
[source,lua]
|
||
----
|
||
local frame = discGetFrame()
|
||
|
||
if frame >= LAST_FRAME then
|
||
startRound()
|
||
frame = FIRST_FRAME
|
||
end
|
||
----
|
||
|
||
La vidéo est l'horloge, comme à la leçon quatorze, et `discGetFrame` est la
|
||
façon de la lire. Tout le reste de l'image est décidé à partir de ce seul
|
||
nombre. Quand le film dépasse la fin de la dernière cible, la manche recommence :
|
||
`startRound` remet le score à zéro, marque chaque cible comme non touchée, et
|
||
renvoie le disque au début avec `discSkipToFrame`. La ligne en dessous règle
|
||
aussi la copie locale de `frame`, parce qu'elle a été lue avant le saut et
|
||
contient encore l'ancien nombre. Sans elle, le dessin plus bas passerait une image à travailler avec un nombre qui
|
||
n'est plus vrai.
|
||
|
||
[source,lua]
|
||
----
|
||
shots = shots + 1
|
||
|
||
for _, target in ipairs(targets) do
|
||
if targetIsUp(target, frame) and collidePointRect(...) then
|
||
----
|
||
|
||
Comptez le tir avant de chercher une touche, pour qu'un tir raté compte aussi.
|
||
Puis parcourez la liste. Le `return` dans la boucle s'arrête à la première
|
||
cible touchée, ce qui veut dire qu'une balle ne peut pas abattre deux cibles qui
|
||
se chevauchent -- une règle que vous devez trancher dans un sens ou dans l'autre,
|
||
et c'est cette ligne qui la tranche.
|
||
|
||
[source,lua]
|
||
----
|
||
overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1)
|
||
----
|
||
|
||
`overlayBox` veut deux coins opposés, pas un coin et une taille, donc le coin
|
||
éloigné est le coin proche plus la taille. Le `- 1` est là parce que les deux
|
||
coins sont inclus : une boîte de `80` à `175` fait quatre-vingt-seize pixels de
|
||
large. `collidePointRect` veut au contraire un coin et une taille, c'est
|
||
pourquoi `TARGET_SIZE` y entre tout seul et qu'ici il est ajouté au coin.
|
||
Confondre les deux est un bug vraiment courant, et il se voit sous forme de
|
||
cibles que vous pouvez toucher un peu en dehors de l'endroit où elles sont
|
||
dessinées.
|
||
|
||
[source,lua]
|
||
----
|
||
if flash > 0 then
|
||
colorForeground(255, 255, 255, 255)
|
||
overlayCircle(flashX, flashY, 30 - flash * 2)
|
||
flash = flash - 1
|
||
end
|
||
----
|
||
|
||
`flash` est un compte à rebours en images, mis à douze quand un tir fait mouche
|
||
et diminué de un chaque fois que l'écran est dessiné. Comme le rayon est
|
||
calculé *à partir* du compte à rebours, l'anneau grandit à mesure que le nombre
|
||
diminue. Douze images, c'est moins d'une demi-seconde. Un retour visuel qui dure
|
||
plus longtemps que ça cesse de ressembler à une conséquence de la gâchette et
|
||
commence à ressembler à la météo.
|
||
|
||
[#_two_guns]
|
||
=== Deux pistolets
|
||
|
||
Vous n'êtes qu'à deux changements d'une borne à deux joueurs, et aucun des deux
|
||
n'est dans la boucle du jeu.
|
||
|
||
Le premier est `mouseSetMode(MOUSE_MANY)`, qui dit à Singe de lire chaque
|
||
appareil séparément au lieu de les réunir en un seul curseur. `mouseHowMany()`
|
||
dit combien il en a trouvé au démarrage, donc un jeu demande normalement avant
|
||
de s'engager :
|
||
|
||
[source,lua]
|
||
----
|
||
if mouseHowMany() >= 2 then
|
||
mouseSetMode(MOUSE_MANY)
|
||
end
|
||
----
|
||
|
||
Après ça, `mouseGetPosition(1)` est le second pistolet, et l'argument `device`
|
||
de `onInputPressed` dit quel pistolet a tiré. Gardez deux réticules, deux
|
||
scores, et une seule liste de cibles, et vous avez un jeu coopératif.
|
||
|
||
Le second changement n'est pas du tout dans votre script. Le `controls.cfg`
|
||
fourni n'affecte que les boutons de la *première* souris, donc la gâchette du
|
||
second pistolet n'atteint rien. Une borne à deux pistolets a besoin dans ce
|
||
fichier de sa propre ligne, qui nomme les deux :
|
||
|
||
[source,lua]
|
||
----
|
||
INPUT_ACTION_3 = { SCANCODE.LSHIFT, GAMEPAD_0.BUTTON_X, MOUSE_0.BUTTON_LEFT, MOUSE_1.BUTTON_LEFT }
|
||
----
|
||
|
||
Dites-le dans vos instructions. Un joueur dont le second pistolet ne fait rien
|
||
accusera votre jeu, et il aura à moitié raison.
|
||
|
||
[#_the_other_way_to_test_a_hit]
|
||
=== L'autre façon de tester un tir
|
||
|
||
Il y a une seconde façon de répondre à « ce tir a-t-il touché quelque chose ? »,
|
||
et elle vaut d'être connue même si vous n'allez pas vous en servir aujourd'hui.
|
||
`vldpGetPixel(x, y)` lit la couleur d'un pixel de l'image vidéo actuellement à
|
||
l'écran. Des jeux s'en servent depuis la toute première version de Singe :
|
||
peignez chaque cible, dans les images, d'une couleur qui n'apparaît nulle part
|
||
ailleurs, et à chaque pression sur la gâchette, demandez quelle couleur se
|
||
trouve sous le réticule.
|
||
|
||
Ça vous offre des zones d'impact qui suivent exactement l'action, gratuitement,
|
||
sans aucun rectangle à définir. Ça vous coûte des images qu'il faut préparer de
|
||
cette façon, et un test d'impact que vous ne pouvez ni voir ni déboguer.
|
||
L'entrée de la Référence consacrée à `vldpGetPixel` donne les détails. Les rectangles
|
||
sont le bon choix par défaut ; gardez ceci sous le coude.
|
||
|
||
[#_what_only_a_cabinet_cares_about]
|
||
=== Ce qui ne compte que pour une borne
|
||
|
||
Tout ce qui précède marche sur votre bureau. Plusieurs choses n'existent pas du
|
||
tout sur votre bureau, et c'est le bon moment pour les nommer, pour qu'elles ne
|
||
soient pas une surprise plus tard.
|
||
|
||
*Le calibrage.* Il faut dire à un vrai pistolet optique où se trouve l'écran,
|
||
parce que c'est une caméra qui regarde un moniteur depuis l'endroit où se tient
|
||
le joueur. Les outils de service de Singe ont un écran Light Gun qui place des
|
||
cibles au milieu et à chaque coin et marque l'endroit que le pistolet a vraiment
|
||
visé, pour que vous voyiez dans quel sens il est décalé. Votre jeu ne fait pas
|
||
le calibrage et ne devrait pas essayer.
|
||
|
||
*La bordure Sinden.* Une famille de pistolets répandue trouve l'écran en
|
||
cherchant un cadre lumineux dessiné autour de l'image. `--sindengun` dessine ce
|
||
cadre, et le moteur ramène les positions dans l'image plus petite qui se trouve
|
||
à l'intérieur, pour que votre jeu continue de recevoir des coordonnées qui
|
||
veulent dire ce qu'elles voulaient dire avant.
|
||
|
||
*Les écrans qui n'ont pas la forme du film.* `ratioGetX()` et `ratioGetY()`
|
||
vous donnent les nombres qu'un joueur a passés en ligne de commande pour un
|
||
écran dont les proportions ne correspondent pas aux images. Singe ne les
|
||
applique pas ; un jeu qui en veut fait le calcul lui-même.
|
||
|
||
*Les pistolets qui s'en vont.* `SWITCH_MOUSE_DISCONNECT` arrive dans
|
||
`onInputPressed` quand un pistolet est débranché. Aucun bouton ne le produit ;
|
||
c'est le moteur qui le déclenche. Un jeu qui le guette peut afficher
|
||
« PLAYER 2 GUN DISCONNECTED » à l'écran au lieu d'avoir l'air d'être mort.
|
||
|
||
*Pas de souris du tout.* Une borne avec un joystick et sans appareil de
|
||
pointage peut piloter le curseur depuis le stick, ce que `joyMouseEnable`
|
||
active.
|
||
|
||
Rien de tout ça ne change le jeu que vous avez écrit. Tout ça, c'est la leçon
|
||
trente et un.
|
||
|
||
[#_try_it_17]
|
||
=== Essayez
|
||
|
||
. *Déplacez une cible.* Changez le `x` de la troisième cible en `680` et lancez.
|
||
Elle déborde maintenant en bonne partie du bord droit, parce que l'overlay
|
||
fait 720 de large et la cible 96. Vous pouvez toujours tirer sur le petit bout
|
||
qui reste. Décidez de ce que vous voulez qu'il se passe sur les bords.
|
||
. *Rendez les cibles plus faciles.* Changez `TARGET_SIZE` en `140`. Remarquez
|
||
que le dessin et le test d'impact changent tous les deux, et que vous n'avez
|
||
modifié qu'un seul nombre. C'est à ça que sert une constante nommée.
|
||
. *Comptez la précision.* Ajoutez une ligne qui affiche `hits` en pourcentage de
|
||
`shots`. Protégez-vous contre une division par zéro avant le premier tir, et
|
||
découvrez ce que Lua affiche si vous ne le faites pas.
|
||
. *Faites qu'une cible vous punisse.* Ajoutez un champ `penalty = true` à un
|
||
enregistrement et retirez des points quand cette cible est touchée. Remarquez
|
||
que vous ajoutez un champ à un seul enregistrement, pas aux quatre, et que
|
||
ceux qui ne l'ont pas y ont `nil`.
|
||
. *Retirez le `return`* de la boucle dans `onInputPressed`, puis faites se
|
||
chevaucher deux cibles en leur donnant les mêmes images et presque la même
|
||
position. Un seul tir les abat maintenant toutes les deux. Décidez du
|
||
comportement que veut votre jeu.
|
||
|
||
[#_break_it_on_purpose_16]
|
||
=== Cassez-le exprès
|
||
|
||
Changez le test de tir pour qu'il utilise `SWITCH_BUTTON1` au lieu de
|
||
`SWITCH_BUTTON3` et lancez. Rien ne casse. Aucune erreur n'apparaît. Le bouton
|
||
gauche ne fait plus rien, et c'est le bouton *droit* qui tire maintenant.
|
||
|
||
C'est la pire sorte de bug, et la plus courante dans le code des entrées : le
|
||
programme n'est pas faux, il est juste branché sur la mauvaise chose. Il n'y a
|
||
pas de message d'erreur à lire, parce que vous avez posé une question sensée sur
|
||
un contact qui existe, et il a répondu honnêtement.
|
||
|
||
Deux choses permettent de le trouver. La première est `debugPrint`, de la leçon
|
||
huit :
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what, device)
|
||
debugPrint("switch " .. what .. " from device " .. tostring(device))
|
||
----
|
||
|
||
Maintenant, chaque appui affiche une ligne, et vous pouvez voir ce que la
|
||
gâchette envoie vraiment. Remarquez `tostring(device)` : `device` vaut `nil`
|
||
pour un appui sur une touche, et coller `nil` à une chaîne avec `..` est une
|
||
erreur. `tostring` le transforme en mot `nil` et la ligne s'affiche.
|
||
|
||
La seconde est l'Input Test du moteur, dans les outils de service, qui montre
|
||
chaque appareil qu'il voit et le dernier contact que le jeu aurait reçu. Quand
|
||
un bouton ne fait rien, cet écran vous dit quelle moitié du problème vous avez :
|
||
soit le contact arrive et votre jeu l'ignore, soit il n'arrive pas du tout.
|
||
|
||
[#_what_you_learned_17]
|
||
=== Ce que vous avez appris
|
||
|
||
* Un pistolet optique se présente comme une souris, donc un jeu écrit pour la
|
||
souris marche avec un pistolet.
|
||
* `mouseGetPosition(device)` renvoie deux valeurs, la position du pointeur en
|
||
coordonnées de l'overlay.
|
||
* Une fonction Lua peut renvoyer plus d'une valeur, et vous les attrapez en
|
||
écrivant autant de noms.
|
||
* `singeWantsCrosshairs()` dit s'il faut dessiner votre propre réticule.
|
||
Lisez-le une fois.
|
||
* Singe capture et cache le pointeur du système au démarrage ; vous n'avez pas à
|
||
le faire.
|
||
* `onInputPressed` reçoit un second argument qui indique quelle souris ou quel
|
||
pistolet a tiré, et `nil` quand c'était une touche ou une manette.
|
||
* Un test d'impact contre un rectangle, c'est `collidePointRect`, la même
|
||
collision que celle que vous avez apprise à la leçon onze.
|
||
* La fenêtre d'images d'une cible, ce ne sont que deux champs de plus dans son
|
||
enregistrement.
|
||
* `a or b` vous donne `b` quand `a` vaut `nil`.
|
||
* Le calibrage, la bordure Sinden et le câblage à deux pistolets vivent en
|
||
dehors de votre script.
|
||
|
||
[#_next_time_17]
|
||
=== La prochaine fois
|
||
|
||
Votre stand de tir demande une seule chose : touchez ça, là. L'autre convention
|
||
d'arcade sur laquelle reposent les jeux en vidéo demande quelque chose de plus
|
||
difficile -- appuyez sur *ceci*, *maintenant*, et vous avez une demi-seconde. La
|
||
leçon dix-huit construit un quick-time event, avec une invite, une fenêtre
|
||
mesurée en images, et trois fins différentes selon que vous avez eu raison, que
|
||
vous avez eu tort, ou que vous avez été trop lent.
|