singe/docs/fr/lessons/17-light-guns.adoc

596 lines
23 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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