627 lines
22 KiB
Text
627 lines
22 KiB
Text
[#_lesson_18_quick_time_events]
|
||
== Leçon 18 : Les quick-time events
|
||
|
||
image::learn/18-qte.png[La leçon terminée, 480]
|
||
|
||
Une invite apparaît. Vous avez une demi-seconde. Appuyez sur la bonne chose et
|
||
l'histoire continue ; appuyez sur la mauvaise, ou sur rien du tout, et elle part
|
||
ailleurs.
|
||
|
||
C'est toute l'idée, et un nombre surprenant de jeux en vidéo ne sont faits de
|
||
presque rien d'autre. Vous avez déjà toutes les pièces : la leçon quatorze vous
|
||
a donné les images, la leçon six une liste d'enregistrements, la leçon trois les
|
||
entrées, et la leçon seize les embranchements. Cette leçon les assemble et
|
||
ajoute une chose vraiment nouvelle -- une fenêtre de temps, et ce qui se passe
|
||
quand elle se ferme.
|
||
|
||
[#_the_clock_is_the_film]
|
||
=== L'horloge, c'est le film
|
||
|
||
Tout ici se mesure en *images*, pas en secondes, et ça vaut la peine de s'y
|
||
arrêter, parce que c'est la décision qui, à elle seule, fait que tout ça marche.
|
||
|
||
La leçon quatorze disait qu'une image est l'adresse d'un moment du film.
|
||
L'invite doit apparaître quand l'acteur commence à frapper, et la fenêtre doit
|
||
se fermer quand le poing arrive. Ce ne sont pas des instants, ce sont des
|
||
endroits dans les images, et vous les avez trouvés en les parcourant pas à pas.
|
||
Si vous écrivez la fenêtre en secondes, vous devez lancer un chronomètre quand
|
||
l'invite apparaît, et alors les deux horloges -- la vôtre et celle du film --
|
||
peuvent se décaler. Si la vidéo saccade sur une machine lente, votre chronomètre
|
||
continue de tourner et le joueur est puni pour son matériel.
|
||
|
||
Les images ne peuvent pas se décaler, parce qu'il n'y a qu'une seule horloge.
|
||
`discGetFrame()` est l'heure, toujours. Une fenêtre, c'est deux numéros d'image,
|
||
et « combien de temps reste-t-il », c'est une soustraction.
|
||
|
||
Lancez avec la même vidéo qu'avant :
|
||
|
||
----
|
||
Singe -R -v Singe/menuBackground.mkv moves
|
||
----
|
||
|
||
Toujours la grille violette et le coucher de soleil. Imaginez un combat.
|
||
|
||
[#_a_move_is_a_record]
|
||
=== Un mouvement est un enregistrement
|
||
|
||
Voici la table sur laquelle repose toute la leçon :
|
||
|
||
[source,lua]
|
||
----
|
||
local moves = {
|
||
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
||
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
||
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
||
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
||
}
|
||
----
|
||
|
||
Quatre mouvements. Chacun dit quand la fenêtre s'ouvre, quand elle se ferme,
|
||
quel contact y répond, et quoi mettre à l'écran. Quarante-cinq images, c'est
|
||
environ une seconde et demie à la fréquence d'images de cette vidéo, ce qui est
|
||
généreux ; les vrais jeux vous en donnent souvent moins de la moitié.
|
||
|
||
Tout ce qui fait qu'un mouvement est un mouvement se trouve dans cette table. Il
|
||
n'y a de code nulle part plus bas qui sache qu'il y a quatre mouvements, ou que
|
||
le deuxième est vers le haut, ou que la première fenêtre s'ouvre à l'image
|
||
soixante. Ajouter un cinquième mouvement, c'est ajouter une ligne. Changer le
|
||
rythme de tous les mouvements, c'est modifier des nombres. Personne n'a besoin
|
||
de lire une fonction pour changer le combat.
|
||
|
||
Ce n'est pas une astuce propre à cette leçon. C'est la différence entre un jeu
|
||
que vous pouvez régler et un jeu que vous ne pouvez pas régler, et ça ne coûte
|
||
rien de le faire dès le début. Quand vous vous surprenez sur le point d'écrire
|
||
`if frame == 60 then` pour la quatrième fois, arrêtez-vous et faites plutôt une
|
||
table.
|
||
|
||
[#_what_happens_when]
|
||
=== Ce qui se passe, et quand
|
||
|
||
Trois choses peuvent arriver à un mouvement, et ce sont trois issues
|
||
différentes, pas deux :
|
||
|
||
* Le joueur appuie sur le bon contact pendant la fenêtre. Comptez les points et
|
||
continuez.
|
||
* Le joueur appuie sur un *mauvais* contact pendant la fenêtre. Il a essayé et
|
||
il a échoué.
|
||
* La fenêtre se ferme sans aucune réponse. Il est resté figé.
|
||
|
||
Le code paresseux traite les deux derniers cas de la même façon. Les jeux ne le
|
||
font presque jamais, parce qu'à jouer, ils font un effet complètement
|
||
différent : se tromper est une erreur, rester figé en est une autre, et le film
|
||
part généralement ailleurs pour chacune. Ici, la mauvaise réponse envoie le
|
||
disque à l'image `362` et l'absence de réponse l'envoie à l'image `388`, ce qui,
|
||
avec ces images-là, veut dire deux quantités de coucher de soleil un peu
|
||
différentes. Avec de vraies images, ce seraient deux plans différents de vous en
|
||
train de perdre.
|
||
|
||
Chaque enregistrement gagne donc un cinquième champ pendant la partie --
|
||
`answer` -- qui vaut `nil` jusqu'à ce qu'il arrive quelque chose au mouvement,
|
||
puis l'une de trois chaînes :
|
||
|
||
[source,lua]
|
||
----
|
||
move.answer = "right"
|
||
move.answer = "wrong"
|
||
move.answer = "late"
|
||
----
|
||
|
||
`nil` est la valeur que Lua vous donne pour un champ que personne n'a rempli.
|
||
Vous l'avez rencontré à la leçon trois. Ici, il mérite sa place : « pas encore de
|
||
réponse » est vraiment un état différent de chacune des trois réponses, et `nil`
|
||
le dit sans que vous ayez à inventer un mot pour ça.
|
||
|
||
[#_finding_the_move_that_is_being_asked]
|
||
=== Trouver le mouvement demandé
|
||
|
||
Tout le reste a besoin de savoir une chose : quel mouvement, s'il y en a un, est
|
||
ouvert en ce moment ?
|
||
|
||
[source,lua]
|
||
----
|
||
local function moveAt(frame)
|
||
for _, move in ipairs(moves) do
|
||
if move.answer == nil and frame >= move.first and frame <= move.last then
|
||
return move
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
----
|
||
|
||
Parcourez la liste, et répondez avec le premier mouvement dont la fenêtre couvre
|
||
cette image et qui n'a pas encore reçu de réponse. Si rien ne correspond,
|
||
répondez `nil`.
|
||
|
||
Renvoyer `nil` exprès, ça mérite d'être remarqué. Une fonction qui parfois n'a
|
||
pas de réponse devrait le dire, et « aucun mouvement n'est ouvert » est une très
|
||
bonne réponse. Le code qui l'appelle écrit alors `if move ~= nil then`, et le
|
||
code qui dessine l'invite ne s'exécute jamais quand il n'y a rien à demander.
|
||
|
||
Le test `move.answer == nil` est ce qui empêche qu'un mouvement soit demandé
|
||
deux fois. Dès que le joueur a la bonne réponse, l'invite disparaît même si la
|
||
fenêtre est encore ouverte -- et c'est exactement ce qu'elle doit faire,
|
||
puisqu'il a répondu.
|
||
|
||
[#_reading_the_answer]
|
||
=== Lire la réponse
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if not isDirection(what) then
|
||
return
|
||
end
|
||
|
||
local move = moveAt(discGetFrame())
|
||
if move == nil then
|
||
return
|
||
end
|
||
|
||
if what == move.switch then
|
||
move.answer = "right"
|
||
score = score + RIGHT_SCORE
|
||
else
|
||
move.answer = "wrong"
|
||
endRun("WRONG WAY", WRONG_FRAME)
|
||
end
|
||
end
|
||
----
|
||
|
||
Lisez-le de haut en bas. Si le contact n'est pas l'une des quatre directions, ça
|
||
ne nous regarde pas -- on s'en va. Si aucun mouvement n'est ouvert, un appui sur
|
||
une direction ne veut rien dire -- on s'en va. Sinon, il y a exactement un
|
||
mouvement auquel comparer, et une seule comparaison à faire.
|
||
|
||
Ces deux lignes `return` du début sont des garde-fous : elles écartent les cas
|
||
qui ne vous intéressent pas, pour que le code en dessous ne s'exécute jamais que
|
||
dans la seule situation pour laquelle il a été écrit. L'écrire dans l'autre
|
||
sens, avec le vrai travail imbriqué sur trois niveaux à l'intérieur de trois
|
||
blocs `if`, dit la même chose et se lit beaucoup plus difficilement.
|
||
|
||
`isDirection` est une toute petite fonction qui ne fait rien d'autre que garder
|
||
cette première ligne courte :
|
||
|
||
[source,lua]
|
||
----
|
||
local function isDirection(what)
|
||
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
||
end
|
||
----
|
||
|
||
Remarquez qu'elle renvoie la comparaison elle-même au lieu de dire
|
||
`if ... then return true else return false end`. Une comparaison vaut déjà
|
||
`true` ou `false`. Tester si quelque chose est vrai pour pouvoir dire que c'est
|
||
vrai, c'est une habitude qu'il vaut mieux perdre tôt.
|
||
|
||
[#_the_window_closing]
|
||
=== La fenêtre qui se ferme
|
||
|
||
Personne n'appuie sur rien, et les images continuent de défiler. Quelque chose
|
||
doit le remarquer. Ce quelque chose, c'est `onOverlayUpdate`, parce qu'il
|
||
s'exécute à chaque image, que le joueur fasse quelque chose ou non :
|
||
|
||
[source,lua]
|
||
----
|
||
if not runOver then
|
||
for _, move in ipairs(moves) do
|
||
if move.answer == nil and frame > move.last then
|
||
move.answer = "late"
|
||
endRun("TOO SLOW", LATE_FRAME)
|
||
break
|
||
end
|
||
end
|
||
end
|
||
----
|
||
|
||
Un mouvement sans réponse dont la dernière image est passée a été manqué.
|
||
`break` quitte la boucle immédiatement, parce qu'une fois la partie finie, ça ne
|
||
sert à rien de regarder le reste.
|
||
|
||
`endRun` est l'embranchement, et c'est le même embranchement que celui que vous
|
||
avez écrit à la leçon seize :
|
||
|
||
[source,lua]
|
||
----
|
||
local function endRun(reason, frame)
|
||
verdict = reason
|
||
runOver = true
|
||
discSkipToFrame(frame)
|
||
end
|
||
----
|
||
|
||
Trois lignes. Retenir quoi dire, retenir que la partie est finie, et envoyer le
|
||
film ailleurs. Un quick-time event manqué n'est pas un mécanisme spécial. C'est
|
||
un choix que le joueur a fait en n'en faisant pas, et le film bifurque dessus
|
||
exactement comme il bifurque sur une porte qu'il a choisie.
|
||
|
||
[#_a_gauge_that_drains]
|
||
=== Une jauge qui se vide
|
||
|
||
Le joueur ne peut pas voir les numéros d'image. Il a besoin de voir le temps
|
||
s'écouler, et de le voir sans quitter l'action des yeux, ce qui veut dire une
|
||
forme qui change plutôt qu'un nombre qui compte.
|
||
|
||
Deux nombres vous donnent tout :
|
||
|
||
[source,lua]
|
||
----
|
||
local left = move.last - frame
|
||
local span = move.last - move.first
|
||
----
|
||
|
||
`span` est la longueur de la fenêtre. `left` est ce qu'il en reste. La barre
|
||
fait `left` sur `span` de la largeur totale :
|
||
|
||
[source,lua]
|
||
----
|
||
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
||
----
|
||
|
||
`//` est une division qui jette la partie fractionnaire, ce que vous voulez ici,
|
||
parce qu'une barre de cent trente-trois pixels et un tiers de large, ça
|
||
n'existe pas. Multipliez avant de diviser -- `GAUGE_W * left // span` et non
|
||
`GAUGE_W * (left // span)` -- sinon la fraction est jetée alors qu'elle est
|
||
encore la seule information que vous avez, et la barre saute de pleine à vide
|
||
sans rien entre les deux.
|
||
|
||
Il n'y a pas de rectangle plein dans l'overlay. `overlayBox` dessine un contour,
|
||
ce qui est ce que vous voulez pour le cadre autour de la jauge, et le
|
||
remplissage est à votre charge :
|
||
|
||
[source,lua]
|
||
----
|
||
local function fillBar(x, y, width, height)
|
||
if width < 1 then
|
||
return
|
||
end
|
||
for row = 0, height - 1 do
|
||
overlayLine(x, y + row, x + width - 1, y + row)
|
||
end
|
||
end
|
||
----
|
||
|
||
Dix-huit traits horizontaux empilés les uns sur les autres. Le garde-fou du
|
||
début compte : à la toute dernière image de la fenêtre, `left` vaut zéro, la
|
||
largeur vaut zéro, et sans lui, `x + width - 1` est un pixel à *gauche* de `x`
|
||
et vous dessinez un petit trait à l'envers au lieu de rien.
|
||
|
||
La touche finale, c'est un changement de couleur quand ça devient désespéré :
|
||
|
||
[source,lua]
|
||
----
|
||
if left * 3 < span then
|
||
colorForeground(255, 60, 60, 255)
|
||
else
|
||
colorForeground(255, 200, 0, 255)
|
||
end
|
||
----
|
||
|
||
La barre devient rouge dans son dernier tiers. Écrit comme ça -- en multipliant
|
||
le temps restant plutôt qu'en divisant la durée -- tout reste en nombres entiers
|
||
et ça marche pour une fenêtre de n'importe quelle longueur.
|
||
|
||
[#_the_whole_thing_10]
|
||
=== Le tout
|
||
|
||
[source,lua]
|
||
----
|
||
-- Apprendre à programmer avec Singe -- Leçon 18 : Les quick-time events
|
||
|
||
dofile("Singe/Framework.singe")
|
||
|
||
overlaySetResolution(discGetWidth(), discGetHeight())
|
||
|
||
local FIRST_FRAME = 20
|
||
local WRONG_FRAME = 362
|
||
local LATE_FRAME = 388
|
||
local LAST_FRAME = 415
|
||
|
||
local PROMPT_COL = 54
|
||
local PROMPT_ROW = 22
|
||
local GAUGE_X = 210
|
||
local GAUGE_Y = 330
|
||
local GAUGE_W = 300
|
||
local GAUGE_H = 18
|
||
|
||
local RIGHT_SCORE = 100
|
||
|
||
local moves = {
|
||
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
||
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
||
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
||
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
||
}
|
||
|
||
local score = 0
|
||
local runOver = false
|
||
local verdict = ""
|
||
|
||
|
||
local function fillBar(x, y, width, height)
|
||
if width < 1 then
|
||
return
|
||
end
|
||
for row = 0, height - 1 do
|
||
overlayLine(x, y + row, x + width - 1, y + row)
|
||
end
|
||
end
|
||
|
||
|
||
local function isDirection(what)
|
||
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
||
end
|
||
|
||
|
||
local function moveAt(frame)
|
||
for _, move in ipairs(moves) do
|
||
if move.answer == nil and frame >= move.first and frame <= move.last then
|
||
return move
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
|
||
local function startRun()
|
||
score = 0
|
||
runOver = false
|
||
verdict = ""
|
||
for _, move in ipairs(moves) do
|
||
move.answer = nil
|
||
end
|
||
discSkipToFrame(FIRST_FRAME)
|
||
end
|
||
|
||
|
||
local function endRun(reason, frame)
|
||
verdict = reason
|
||
runOver = true
|
||
discSkipToFrame(frame)
|
||
end
|
||
|
||
|
||
function onInputPressed(what)
|
||
if runOver then
|
||
if what == SWITCH_START1 then
|
||
startRun()
|
||
end
|
||
return
|
||
end
|
||
|
||
if not isDirection(what) then
|
||
return
|
||
end
|
||
|
||
local move = moveAt(discGetFrame())
|
||
if move == nil then
|
||
return
|
||
end
|
||
|
||
if what == move.switch then
|
||
move.answer = "right"
|
||
score = score + RIGHT_SCORE
|
||
else
|
||
move.answer = "wrong"
|
||
endRun("WRONG WAY", WRONG_FRAME)
|
||
end
|
||
end
|
||
|
||
|
||
function onOverlayUpdate()
|
||
local frame = discGetFrame()
|
||
|
||
if not runOver then
|
||
for _, move in ipairs(moves) do
|
||
if move.answer == nil and frame > move.last then
|
||
move.answer = "late"
|
||
endRun("TOO SLOW", LATE_FRAME)
|
||
break
|
||
end
|
||
end
|
||
end
|
||
|
||
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
||
if not runOver then
|
||
verdict = "CLEAR"
|
||
runOver = true
|
||
end
|
||
discSearch(LAST_FRAME)
|
||
end
|
||
|
||
overlayClear()
|
||
|
||
local move = nil
|
||
if not runOver then
|
||
move = moveAt(frame)
|
||
end
|
||
|
||
if move ~= nil then
|
||
local left = move.last - frame
|
||
local span = move.last - move.first
|
||
|
||
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
||
|
||
colorForeground(255, 255, 255, 255)
|
||
overlayBox(GAUGE_X - 2, GAUGE_Y - 2, GAUGE_X + GAUGE_W + 1, GAUGE_Y + GAUGE_H + 1)
|
||
|
||
if left * 3 < span then
|
||
colorForeground(255, 60, 60, 255)
|
||
else
|
||
colorForeground(255, 200, 0, 255)
|
||
end
|
||
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
||
end
|
||
|
||
overlayPrint(2, 1, "SCORE " .. score .. " FRAME " .. frame)
|
||
|
||
if runOver then
|
||
overlayPrint(2, 3, verdict)
|
||
overlayPrint(2, 4, "PRESS 1 TO RUN IT AGAIN")
|
||
end
|
||
|
||
return OVERLAY_UPDATED
|
||
end
|
||
|
||
|
||
startRun()
|
||
----
|
||
|
||
Les touches fléchées répondent aux invites. Réussissez les quatre et le film va
|
||
jusqu'au bout et s'y arrête sur `CLEAR`. Ratez-en une, ou restez les bras
|
||
croisés, et il saute à une autre image avec un autre mot à l'écran. `1` relance
|
||
le tout.
|
||
|
||
[#_what_just_happened_14]
|
||
=== Ce qui vient de se passer
|
||
|
||
Vous en avez déjà lu presque tout. Quatre morceaux méritent qu'on y revienne.
|
||
|
||
[source,lua]
|
||
----
|
||
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
||
----
|
||
|
||
La fin du film doit être attrapée, sinon il dépasse la fin et il n'y a plus rien
|
||
à voir. `discSearch` va à une image et s'y *met en pause*, ce qui est exactement
|
||
ce que veut un plan final. La moitié `discGetState()` empêche que ça se
|
||
reproduise à chacune des images suivantes : une fois le disque en pause, il
|
||
n'est plus `DISC_PLAYING`, donc la condition est fausse et le saut n'est pas
|
||
répété soixante fois par seconde. `discGetState` et les constantes `DISC_*` sont
|
||
dans la Référence, à l'entrée `discGetState`.
|
||
|
||
[source,lua]
|
||
----
|
||
local move = nil
|
||
if not runOver then
|
||
move = moveAt(frame)
|
||
end
|
||
----
|
||
|
||
`onOverlayUpdate` redemande le mouvement ouvert, au lieu que `onInputPressed` le
|
||
lui dise. C'est voulu. Il y a exactement une fonction qui sait quel mouvement
|
||
est ouvert, et tout le monde lui pose la question. Si l'invite venait d'une
|
||
variable que le code des entrées tient à jour, alors deux morceaux de code
|
||
retiendraient le même fait, et tôt ou tard l'un d'eux se tromperait. Quand vous
|
||
avez le choix entre demander et retenir, demandez.
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
||
----
|
||
|
||
`overlayPrint` compte en cellules de caractère, comme depuis la leçon un, donc
|
||
l'invite est placée à l'œil : la colonne cinquante-quatre est à peu près au
|
||
milieu pour des mots de cette longueur. C'est à peu près, pas juste, et
|
||
« PRESS RIGHT » finit trois caractères plus à droite que « PRESS UP ». La leçon
|
||
dix-neuf charge une vraie police, et avec elle la possibilité de mesurer une
|
||
chaîne en pixels et de la centrer correctement.
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if runOver then
|
||
if what == SWITCH_START1 then
|
||
startRun()
|
||
end
|
||
return
|
||
end
|
||
----
|
||
|
||
Un seul callback, deux tâches complètement différentes, choisies par `runOver`.
|
||
Pendant la partie, les directions veulent dire des mouvements ; une fois qu'elle
|
||
est finie, la seule touche qui veut dire quelque chose, c'est Start. Une
|
||
variable qui décide quelles règles s'appliquent est un *état*, le nom que lui a
|
||
donné la leçon douze, et un jeu n'est en grande partie qu'un petit nombre
|
||
d'états.
|
||
|
||
[#_try_it_18]
|
||
=== Essayez
|
||
|
||
. *Rendez-le plus difficile.* Changez le `last` du premier mouvement de `105` en
|
||
`75`. Quinze images, c'est une demi-seconde. Jouez et voyez si vous y
|
||
arrivez.
|
||
. *Ajoutez un mouvement.* Mettez une cinquième ligne dans la table avec une
|
||
fenêtre entre les images `370` et `400`, et montez `LAST_FRAME` à `418`. Vous
|
||
ne devriez avoir aucune autre ligne à toucher. Si vous devez le faire,
|
||
quelque chose dans le code en sait trop sur la table.
|
||
. *Notez la vitesse.* Donnez plus de points pour une réponse rapide. Dans
|
||
`onInputPressed`, là où la bonne réponse est comptée, calculez `left` et
|
||
`span` à partir de `move` et de `discGetFrame()` de la même façon que la
|
||
jauge, et ajoutez `RIGHT_SCORE * left // span` au lieu de `RIGHT_SCORE`.
|
||
Décidez si ça rend le jeu meilleur ou seulement plus agité.
|
||
. *Donnez à une mauvaise réponse sa propre conséquence.* Au lieu de terminer la
|
||
partie, faites qu'un mauvais appui coûte cinquante points et laissez la
|
||
fenêtre ouverte pour réessayer. Vous devrez arrêter de remplir `move.answer`,
|
||
et vous découvrirez pourquoi on le remplissait au départ.
|
||
. *Deux invites à la fois.* Faites se chevaucher les fenêtres de deux mouvements
|
||
et voyez ce qui se passe. `moveAt` renvoie la première correspondance, donc un
|
||
seul mouvement est demandé à la fois. Décidez de ce que vous voudriez, et de
|
||
ce qu'il faudrait pour y arriver.
|
||
|
||
[#_break_it_on_purpose_17]
|
||
=== Cassez-le exprès
|
||
|
||
Supprimez le `break` de la boucle qui trouve les mouvements manqués, et manquez
|
||
exprès le premier. La partie se termine, le disque saute, et tout a l'air
|
||
correct.
|
||
|
||
Maintenant, mettez `debugPrint(move.name .. " missed")` à côté de
|
||
`move.answer = "late"` et recommencez. La console affiche une ligne, comme vous
|
||
vous y attendez.
|
||
|
||
Puis retirez aussi le `if not runOver then` autour de la boucle, et manquez de
|
||
nouveau le premier mouvement :
|
||
|
||
----
|
||
LEFT missed
|
||
UP missed
|
||
RIGHT missed
|
||
DOWN missed
|
||
----
|
||
|
||
La première ligne arrive quand la fenêtre se ferme, comme avant. Les trois
|
||
autres arrivent ensemble dès l'image suivante, à l'instant où le disque atterrit
|
||
à 388 : chaque fenêtre restante est maintenant dans le passé, donc chaque
|
||
mouvement sans réponse est manqué d'un coup, et chacun appelle `endRun` et
|
||
refait un saut. Le jeu marche encore plus ou moins, et c'est ce qui le rend
|
||
dangereux. Trois sauts de trop en une seule image, trois réécritures du verdict,
|
||
et rien à l'écran ne le dit.
|
||
|
||
Il n'y a pas de message d'erreur ici non plus. La leçon, c'est que ce `break` et
|
||
ce garde-fou ne sont pas de la coquetterie. Ce sont les deux lignes qui disent
|
||
« c'est déjà décidé », et du code qui continue de décider quelque chose qu'il a
|
||
déjà décidé est l'une des sources de bugs les plus fiables que vous écrirez
|
||
jamais.
|
||
|
||
[#_what_you_learned_18]
|
||
=== Ce que vous avez appris
|
||
|
||
* Un quick-time event, c'est une invite, une fenêtre, et une conséquence.
|
||
* Mesurez la fenêtre en images, parce que le film est l'horloge et qu'il ne doit
|
||
jamais y avoir qu'une seule horloge.
|
||
* Gardez les mouvements dans une table d'enregistrements, pour qu'ajouter un
|
||
mouvement, ce soit ajouter une ligne.
|
||
* Juste, faux et pas de réponse sont trois issues, et elles mènent généralement
|
||
à trois endroits différents.
|
||
* `nil` dans un champ d'enregistrement est un vrai état : personne n'a encore
|
||
répondu.
|
||
* Une seule fonction répond à « quel mouvement est ouvert », et tout le monde lui
|
||
pose la question plutôt que d'en garder une copie.
|
||
* Les garde-fous -- un `return` précoce pour les cas qui ne vous intéressent pas
|
||
-- évitent d'imbriquer le vrai travail.
|
||
* `//` divise et jette la partie fractionnaire ; multipliez avant de diviser.
|
||
* `overlayBox` dessine un contour, donc une barre pleine est une pile d'appels à
|
||
`overlayLine`.
|
||
* Un mouvement manqué est un embranchement, et `discSkipToFrame` le prend,
|
||
exactement comme à la leçon seize.
|
||
|
||
[#_next_time_18]
|
||
=== La prochaine fois
|
||
|
||
La troisième partie est terminée. Vous savez lire du film, l'attendre, bifurquer
|
||
dessus, viser dedans, et chronométrer le joueur contre lui. Ce que vous avez
|
||
écrit par-dessus tout ça, c'est la police de console du moteur, qui fait six
|
||
pixels de large et qui est faite pour le diagnostic. La leçon dix-neuf charge
|
||
une vraie police, la mesure, et donne aux mots à l'écran l'air de faire partie
|
||
du jeu.
|