[#_lesson_8_when_it_goes_wrong] == Leçon 8 : Quand ça tourne mal image::learn/08-when-it-goes-wrong.png[La leçon terminée, 480] Vous avez écrit un jeu. À partir de maintenant, vous passerez plus de temps à réparer des programmes qu'à en écrire, et ce n'est pas le signe que quelque chose ne va pas chez vous. C'est le métier. La différence entre quelqu'un qui aime programmer et quelqu'un qui abandonne n'est presque jamais le talent ; c'est de savoir s'il a appris à lire ce que l'ordinateur lui dit. Cette leçon n'a donc rien de nouveau à construire. Ce sont les cinq messages que vous verrez le plus souvent pour le reste de votre vie, ce que chacun veut vraiment dire, et quatre manières de traquer l'erreur qui se cache derrière. Gardez ouvert `dodge.singe` de la leçon sept, parce que vous allez le casser exprès plusieurs fois. [#_two_kinds_of_stop] === Deux sortes d'arrêt Singe s'arrête pour deux raisons différentes, et il le dit de deux manières différentes. Quand le script ne veut même pas démarrer, vous obtenez cette forme : ---- Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock' ---- Lua lit votre fichier entier avant d'en exécuter un seul mot, et ceci veut dire qu'il n'a pas pu comprendre ce qu'il a lu. Rien ne s'est exécuté du tout. Quand le script a bien démarré mais que quelque chose a mal tourné plus tard, vous obtenez plutôt cette forme : ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped') stack traceback: dodge/dodge.singe:98: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- Le jeu tournait. Singe a appelé votre `onOverlayUpdate`, comme il le fait soixante fois par seconde, et cette fois l'appel n'est pas revenu. Les deux formes contiennent les trois mêmes choses, et ce sont les trois choses que vous voulez : un fichier, une ligne et une plainte. Lisez-les dans cet ordre, à chaque fois. Les numéros de ligne de cette leçon viennent du script terminé de la leçon sept. Si les vôtres sont décalés d'une ligne ou deux, votre fichier l'est aussi, et rien dans la lecture d'une erreur ne dépend de ce que le numéro corresponde. [#_the_ones_that_stop_it_before_it_starts] === Celles qui l'arrêtent avant qu'il démarre Ce sont les faciles, et ce sont aussi celles qui font le plus mauvais effet, parce que l'écran reste noir et qu'il n'y a rien à regarder. Allez dans `updatePlaying` et supprimez le mot `then` de la ligne `if spawnTimer <= 0 then`. Enregistrez : ---- Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock' ---- Lua est arrivé au bout de la ligne du `if`, voulait le mot `then`, et a trouvé à la place le début de la ligne suivante -- qui est `spawnBlock()`, et c'est la partie `near 'spawnBlock'`. « Near » veut dire « c'est là que je m'en suis aperçu », et c'est en général la première chose *après* ce que vous avez oublié. Maintenant, une plus méchante. Remettez `then`, et supprimez plutôt le `end` de la dernière ligne de `spawnBlock`. Enregistrez : ---- Error running script: dodge/dodge.singe:148: 'end' expected (to close 'function' at line 58) near ---- La ligne 148 est la fin du fichier, juste après sa dernière ligne. La ligne 148 n'a rien qui cloche. Lua a lu jusqu'au bout -- `` est son nom pour la fin d'un fichier -- en attendant toujours un `end` qui n'est jamais venu, et à ce moment-là il avait avalé toutes les fonctions qui suivent `spawnBlock` comme si elles en faisaient partie. La moitié utile de ce message, c'est `(to close 'function' at line 58)`, qui est l'endroit où commence `spawnBlock`. Quand un message montre la fin du fichier, croyez ce qu'il dit entre parenthèses, pas le numéro de ligne. C'est aussi pour ça que l'indentation compte tant : un `end` manquant est presque invisible dans une suite de lignes, et saute aux yeux dès que tout ce qui est en dessous est décalé d'un cran de trop. Deux autres à savoir reconnaître, parce que vous les rencontrerez toutes les deux cette semaine : ---- Error running script: dodge/dodge.singe:41: unfinished string near '"GAME OVER)' ---- Un guillemet ouvert et jamais fermé, que vous avez rencontré à la leçon un. ---- Error running script: dodge/dodge.singe:135: 'then' expected near '=' ---- Celle-là, c'est `if state = "playing" then`. Un seul `=` donne une valeur à une variable, deux `==` posent une question, et Lua ne vous laisse pas donner une valeur à l'intérieur d'un `if`. La plainte a l'air étrange, et elle veut toujours dire la même chose. [#_attempt_to_index_a_nil_value] === Attempt to index a nil value C'est celle que vous verrez le plus, et de loin. Allez dans `spawnBlock` et faites une faute dans `block` sur la première ligne qui l'utilise, pour qu'elle se lise `blocl.x = math.random(0, screenWidth - BLOCK_SIZE)`. Enregistrez, appuyez sur Espace : ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:60: attempt to index a nil value (global 'blocl') stack traceback: dodge/dodge.singe:60: in function 'spawnBlock' dodge/dodge.singe:92: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- *Indexer*, c'est ce que fait le point. `blocl.x` veut dire « va dans la chose appelée `blocl` et trouve le `x` qui s'y trouve ». On ne peut faire ça qu'avec une table. *Nil*, c'est ainsi que Lua appelle un nom derrière lequel il n'y a rien. Vous ne l'avez pas beaucoup vu parce que vous avez pris soin de donner une valeur à tout, mais chaque nom dont vous ne vous êtes jamais servi vaut déjà `nil`, et taper un nom de travers en invente un tout neuf. Le message entier dit donc : *vous avez utilisé un point sur quelque chose qui n'existe pas*. La partie entre parenthèses est le cadeau. `global 'blocl'` est le nom exact de la chose qui était vide. Cherchez-le dans votre fichier. S'il n'apparaît qu'une fois, c'est une faute d'orthographe, et vous l'avez trouvée en dix secondes. Le mot avant le nom vous dit où Lua l'a cherché : * `global` -- un nom qui n'a de `local` devant lui nulle part. * `local` -- un nom que vous avez déclaré avec `local` dans la fonction où vous êtes. * `field` -- un nom après un point, comme le `y` de `block.y`. * `upvalue` -- un `local` venu de plus loin, que cette fonction peut voir, et c'est ce que sont toutes les variables de `dodge.singe`, puisqu'elles sont déclarées en haut du fichier et utilisées à l'intérieur des fonctions. `field` est l'autre cause courante, et ce n'est pas du tout une faute d'orthographe. Si vous avez écrit `block.hit.when` sans jamais avoir rempli `block.hit`, vous obtenez `attempt to index a nil value (field 'hit')`. Le point qui échoue, c'est le deuxième. Lua vous dit lequel en le nommant. [#_attempt_to_call_a_nil_value] === Attempt to call a nil value Remettez `blocl` comme il faut. Maintenant, faites une faute dans une fonction du moteur : dans `drawGame`, changez `overlayPrint` en `overlayPirnt`. Enregistrez : ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:36: attempt to call a nil value (global 'overlayPirnt') stack traceback: dodge/dodge.singe:36: in function 'drawGame' dodge/dodge.singe:140: in function 'onOverlayUpdate' ---- Même `nil`, autre crime. *Appeler*, c'est ce que font les parenthèses, et on ne peut faire ça qu'avec une fonction. Il n'y a pas de fonction appelée `overlayPirnt`, donc `overlayPirnt` vaut `nil`, donc les parenthèses n'ont rien à appeler. Ça arrive pour quatre raisons, de la plus fréquente à la plus rare : . Vous avez fait une faute dans le nom d'une de vos propres fonctions. . Vous avez fait une faute dans le nom d'une de celles de Singe. Rien dans le moteur ne vérifie votre orthographe au chargement du fichier, donc un nom du moteur mal écrit n'est qu'un nom de plus qui se trouve être vide, et ça ne vous coûte rien tant que la ligne ne s'exécute pas. . La fonction n'existe pas du tout dans Singe -- c'était une supposition raisonnable, et les suppositions raisonnables sont fausses à peu près une fois sur deux. La liste des fonctions de la Référence est le seul moyen d'en être sûr. . Vous l'avez rendue `local` et vous l'avez appelée depuis un endroit situé au-dessus de sa déclaration. C'est exactement pour ça que les fonctions de `dodge.singe` sont globales. Il existe une version bien pire de cette erreur, et vous l'avez rencontrée à la leçon un sans le savoir : écrivez mal le nom d'un *callback* et vous n'obtenez aucun message du tout. `onOverlayUpdated` n'est pas une erreur. C'est une fonction parfaitement correcte dont Singe n'a jamais entendu parler et qu'il n'appellera jamais, alors l'écran reste noir et rien ne se plaint. Quand toute une fonctionnalité ne fait absolument rien, soupçonnez le nom. [#_attempt_to_perform_arithmetic_on_a_nil_value] === Attempt to perform arithmetic on a nil value Remettez `overlayPrint`. Dans `updatePlaying`, faites une faute dans `blockSpeed` sur la ligne de chute, pour qu'elle se lise `block.y = block.y + blockSped` : ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped') stack traceback: dodge/dodge.singe:98: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- L'arithmétique, c'est `+`, `-`, `*` et `/`. On ne peut pas ajouter rien à un nombre. Là encore, les parenthèses nomment la chose vide, et là encore la cause la plus courante est une faute de frappe -- mais celle-ci a une seconde cause qu'il vaut la peine de connaître, parce qu'elle est plus difficile à voir. Une variable à laquelle vous n'avez jamais donné de nombre vaut `nil`, pas zéro. Si vous ajoutez un meilleur score au jeu et écrivez `local hiScore` en haut sans `= 0`, elle contient `nil` jusqu'à ce que quelque chose la remplisse, et le premier `hiScore + score` arrête le jeu. Lua ne fait pas partir les nombres de zéro pour vous. C'est vous qui les faites partir de zéro. Deux proches parents, avec la même cause et le même remède : ---- dodge/dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore') ---- `..` qui joint une chaîne à quelque chose qui n'est pas là. Vous verrez celle-là chaque fois que vous afficherez un score que vous avez oublié de remplir -- ici, `hiScore` ajouté à la ligne du score dans `drawGame`, qui est la ligne 37 maintenant que `local hiScore` se trouve au-dessus. ---- dodge/dodge.singe:105: attempt to compare nil with number ---- Ça, c'est `if score > hiScore then`, placé sous `state = "over"` pour garder le meilleur score à la fin d'une partie, sans rien dans `hiScore`. Remarquez que ce message ne nomme rien entre parenthèses. Quand Lua ne peut pas vous dire quel côté était vide, la section après la suivante vous montre comment le découvrir. [#_an_argument_of_the_wrong_kind] === Un argument du mauvais type Tous les messages jusqu'ici venaient de Lua. Celui-ci vient de Singe, et il a l'air complètement différent, alors ça vaut la peine de le rencontrer exprès. Supposez que vous vouliez voir ce que contient `blocks`, et que vous essayiez de l'afficher. Ajoutez cette ligne dans `drawGame`, sous le score : [source,lua] ---- overlayPrint(1, 3, blocks) ---- ---- 37:overlayPrint: Argument 3 must be a string. ---- Pas de nom de fichier, pas de trace de pile, aucun `attempt to` quoi que ce soit. Une plainte du moteur commence par le numéro de ligne, puis le nom de la fonction qui a refusé, puis ce qu'elle voulait. Ici, on a donné à `overlayPrint` une table là où il lui faut du texte, et elle s'est arrêtée plutôt que de dessiner quelque chose qui n'a pas de sens. Le message cousin compte au lieu de vérifier : ---- 34:overlayBox: Expected 4 argument(s), got 3. ---- C'est un `overlayBox` auquel il manque un coin. Le curieux `argument(s)` est celui du moteur, pas une faute de frappe de votre part. Quand vous obtenez l'un de ces messages, l'entrée de cette fonction dans la Référence est la réponse, et ça vous prendra une minute. Chaque entrée donne les arguments dans l'ordre, avec ce que chacun doit être. Singe les vérifie parce que l'autre possibilité, c'est un jeu qui tourne dix minutes puis dessine n'importe quoi. [#_finding_out_what_is_really_in_there] === Découvrir ce qu'il y a vraiment dedans La moitié du débogage, ce n'est pas du tout lire des erreurs. C'est le cas où rien ne plante et où rien ne marche : des blocs qui n'apparaissent jamais, un joueur qui refuse de bouger, un score coincé à zéro. Pour ça, il vous faut voir à l'intérieur du jeu en marche, et il y a deux manières de le faire. La première, c'est `debugPrint`, qui écrit une ligne dans le terminal depuis lequel vous avez lancé Singe. Prenez la ligne à laquelle vous ne comprenez rien et mettez-en un au-dessus : [source,lua] ---- debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer) ---- Vous regardez maintenant ce qui est réellement là au lieu de ce que vous êtes sûr d'y trouver, et c'est dans cet écart que vivent les bugs. Affichez `playerX` de la même façon et vous verrez aussi la vérité à son sujet : il s'affiche `160.0`, pas `160`, parce qu'une division avec `/` donne toujours en Lua un nombre avec une partie fractionnaire, même quand cette partie vaut zéro. Deux choses vont vous mordre. La première, c'est que `debugPrint` exige du texte ou un nombre ; donnez-lui autre chose et il arrête le jeu comme l'a fait `overlayPrint`. `debugPrint(blocks)` ne vous montrera pas une liste, et `debugPrint("left " .. goingLeft)` tout en haut de la fonction `updatePlaying` ne vous montrera pas non plus un `true` ou un `false` : ---- dodge/dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft') ---- `tostring` est le remède. Il prend absolument n'importe quoi et vous en donne du texte, donc `tostring(goingLeft)` vaut `"true"` ou `"false"`, et la ligne marche. La seconde, c'est que `onOverlayUpdate` tourne soixante fois par seconde, donc un `debugPrint` à l'intérieur affiche soixante lignes par seconde et vous ne pouvez en lire aucune. La Référence le dit dans son entrée : l'appel ne coûte pas cher, mais rien ne le freine. Avant toute chose, mettez un interrupteur devant chaque ligne de débogage que vous ajoutez, pour pouvoir couper le bruit sans avoir à les traquer de nouveau : [source,lua] ---- local DEBUG = true function debugLog(text) if DEBUG then debugPrint(text) end end ---- Chaque `debugLog` du jeu obéit à ce seul `true`. Changez-le en `false` et le jeu se tait ; remettez-le et tout revient. Ensuite, affichez quelque chose quand il se *passe* quelque chose plutôt qu'à chaque image : [source,lua] ---- debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives) ---- Ou affichez avec un minuteur, décompté exactement comme l'apparition des blocs : [source,lua] ---- reportTimer = reportTimer - 1 if reportTimer <= 0 then debugReport() reportTimer = REPORT_FRAMES end ---- Le dossier `learn` contient le tout sous le nom `08-when-it-goes-wrong.singe` : c'est le jeu de la leçon sept avec un rapport environ toutes les deux secondes, une ligne quand vous êtes touché, et une ligne quand la partie se termine. La seconde manière de voir à l'intérieur, c'est de le mettre à l'écran, ce qui vaut mieux pour tout ce qui change à chaque image, parce que soixante lignes par seconde dans un terminal, ça ne sert à rien, alors qu'un nombre qui défile dans le coin se surveille facilement : [source,lua] ---- if DEBUG then overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX) end ---- [#_cutting_the_problem_in_half] === Couper le problème en deux Quand vous n'arrivez pas à voir laquelle de vingt lignes est fausse, arrêtez de regarder et commencez à retirer. Mettez deux tirets au début d'une ligne et Lua ignore tout le reste de la ligne. C'est un *commentaire*, et on s'en sert d'habitude pour se laisser des notes, mais le meilleur usage est celui-ci : [source,lua] ---- -- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) ---- Mettez en commentaire la moitié de `drawGame` et lancez. Si le problème est toujours là, il est dans la moitié que vous avez gardée. S'il a disparu, il est dans la moitié que vous avez retirée. Remettez cette moitié et coupez-*la* en deux. Vingt lignes demandent cinq tours de ce petit jeu, et chaque tour, c'est un enregistrement et un coup d'œil. Ça marche pour autre chose que des lignes. Mettez en commentaire le corps de `updatePlaying` et les blocs arrêtent de bouger, ce qui vous dit si ce que vous traquez est dans le déplacement ou dans le dessin. Mettez en commentaire la branche de `collideRects` et voyez si le jeu arrête de planter. Vous n'essayez de rien réparer pendant que vous faites ça. Vous essayez de découvrir où le problème n'est pas. [#_going_round_faster] === Faire le tour plus vite Tout ceci repose sur la possibilité d'essayer quelque chose en deux secondes, et c'est à ça que sert `-R`. Vous vous en servez depuis la leçon un ; voici ce qu'il fait vraiment, et ça vaut la peine de le savoir maintenant que vous allez vous appuyer dessus. Singe surveille chaque fichier de script que votre jeu a chargé. Quand vous en enregistrez un, le jeu repart du début : tout ce que le script a fabriqué est jeté et votre fichier est exécuté à neuf, dans le temps qu'il faut pour lire le fichier plutôt que dans celui qu'il faut pour démarrer le moteur. `F5` fait la même chose à la demande, sans rien enregistrer. Une touche que vous gardez enfoncée pendant un rechargement est ignorée jusqu'à ce que vous la relâchiez, donc le joueur ne partira pas tout seul après un rechargement. Les deux sortes d'arrêt se comportent différemment ici, et c'est utile de savoir laquelle vous avez sous les yeux sans lire le message. Un script qui ne compile pas est *signalé, et Singe y survit* : la fenêtre reste ouverte et vide, le fichier reste surveillé, et enregistrer la correction ramène le jeu. Un plantage à l'intérieur d'un callback -- tous les messages `attempt to`, et les plaintes du moteur sur les arguments -- fait tomber Singe, et vous le relancez à la main. [#_reading_a_traceback] === Lire une trace de pile Les lignes sous `stack traceback:` sont la liste des fonctions qui étaient en cours quand tout s'est arrêté, la plus intérieure en premier. Voici de nouveau celle de `blocl` : ---- dodge/dodge.singe:60: in function 'spawnBlock' dodge/dodge.singe:92: in function 'updatePlaying' dodge/dodge.singe:136: in function 'onOverlayUpdate' ---- Lisez-la de bas en haut et c'est une phrase. Singe a appelé `onOverlayUpdate`. À la ligne 136, `onOverlayUpdate` a appelé `updatePlaying`. À la ligne 92, `updatePlaying` a appelé `spawnBlock`. À la ligne 60, `spawnBlock` a abandonné. La ligne du haut, c'est l'endroit où ça a cassé. Les lignes en dessous, c'est le chemin pour y arriver, et c'est ce qu'il vous faut quand la ligne du haut a l'air innocente. Une fonction qui ajoute un nombre à `block.y` n'a rien de faux ; une fonction qui a mis un bloc dans la liste sans `y`, si. Seule la piste relie les deux, et la piste est affichée pour vous à chaque fois, sans exception. [#_the_habit] === L'habitude Tout ce qui précède, c'est de la technique. Voici l'habitude, et elle compte davantage : *Changez une chose. Lancez. Regardez ce qui s'est passé.* Quand quelque chose refuse de marcher, la tentation est de changer quatre choses à la fois, parce que l'une d'elles sera forcément la bonne. Faites ça et ça arrête de marcher d'une nouvelle façon, et vous ne savez plus laquelle des quatre en est responsable, ni si deux d'entre elles s'annulent. Vous avez agrandi le problème et vous l'avez mieux caché. Un changement. Lancez. Regardez. Ça a l'air plus lent. C'est la méthode la plus rapide qui existe, et tous les programmeurs expérimentés que vous rencontrerez font comme ça, exactement pour la raison que vous êtes sur le point de découvrir. [#_try_it_8] === Essayez . *Rencontrez-les toutes.* Faites exprès chacune des cinq erreurs de cette leçon dans `dodge.singe`, une à la fois, et lisez le message avant de corriger. Elles ne vous feront plus jamais peur. . *Surveillez les apparitions.* Mettez dans `spawnBlock` un `debugPrint` qui affiche le `x` du nouveau bloc. Jouez trente secondes, puis regardez les nombres. Sont-ils répartis sur tout l'écran, ou préfèrent-ils un côté ? . *Coupez-le.* Changez `DEBUG` en `false` dans le script de la leçon, et vérifiez par vous-même que rien ne s'affiche et que le jeu n'a pas changé. . *Un bug sans erreur.* Dans `updatePlaying`, changez `if spawnTimer <= 0 then` en `if spawnTimer == 0 then`, qui a l'air de vouloir dire la même chose. Lancez et commencez une partie. Il n'y a pas d'erreur, rien ne plante, et pas un seul bloc ne tombe, si bien que vous ne pouvez même pas perdre. Mettez un `debugLog` dans `spawnBlock`, ajoutez `spawnTimer` à la première ligne qu'affiche `debugReport`, et rejouez. Déduisez de ce qu'ils disent, et de ce que l'un d'eux ne dit jamais, pourquoi `==` était la mauvaise question à poser. . *Cassez le nom d'un callback.* Renommez `onInputPressed` en `onInputPress` et lancez. Aucune erreur ne s'affiche, rien ne plante, le joueur refuse de bouger, et Espace ne lance pas de partie. Puis expliquez-vous pourquoi c'est l'erreur la plus dangereuse de cette leçon. [#_what_you_learned_8] === Ce que vous avez appris * Les erreurs ont un fichier, une ligne et une plainte. Lisez-les dans cet ordre. * `Error running script` veut dire que rien ne s'est exécuté ; la forme avec une trace de pile veut dire que ça tournait et que ça s'est arrêté. * `nil` est un nom derrière lequel il n'y a rien, et il est derrière la plupart des messages d'erreur que vous verrez jamais. * Le mot entre parenthèses nomme la chose vide, et dit si c'était une variable globale, une variable locale, un champ ou une upvalue. * Un `end` manquant est signalé à la fin du fichier, et les parenthèses vous disent où est vraiment l'erreur. * Les plaintes du moteur ont un autre air : un numéro de ligne, un nom de fonction, et ce qu'elle voulait à la place. * `debugPrint` vous montre ce que contient vraiment une variable. `tostring` rend n'importe quoi affichable, et un interrupteur comme `DEBUG` coupe le bruit. * Mettez une moitié en commentaire avec `--` pour découvrir dans quelle moitié est le problème. * Un callback mal nommé ne produit aucune erreur, ce qui le rend pire qu'un callback qui en produit une. * Changez une chose, lancez, regardez. [#_next_time_8] === La prochaine fois C'est la fin de la première partie. Vous savez écrire un jeu, et maintenant vous savez en réparer un, ce qui veut dire que tout ce qui suit est un ajout et non une fondation. La deuxième partie commence par remplacer ces boîtes de couleur par de vraies images, et à partir de là, le jeu que vous construisez commence à ressembler à un jeu auquel quelqu'un d'autre aurait envie de jouer.