[#_lesson_14_playing_video] == Leçon 14 : Lire de la vidéo image::learn/14-video.png[La leçon terminée, 480] Singe peut lire un film et faire tourner votre jeu à l'intérieur. L'image devient le monde, et l'overlay sur lequel vous dessinez depuis la leçon un est posé par-dessus. Dans cette leçon, vous allez lancer une vidéo, découvrir quelle image est affichée, l'arrêter, et sauter à n'importe quel moment de votre choix. Avant tout cela, une chose doit être dite clairement, parce qu'elle décide si vous devez lire cette partie du livre ou non. [#_a_game_plays_video_only_if_it_asks_to] === Un jeu ne lit de la vidéo que s'il le demande Singe est un moteur de jeu. Tout ce que vous avez construit dans les première et deuxième parties -- les sprites, le son, les collisions, le score, le dossier du jeu -- c'est Singe tout entier, et rien de tout cela ne fait intervenir la vidéo. Ce qu'ajoute la troisième partie est un supplément : un jeu peut demander qu'un film soit lu derrière lui, et alors le film est le monde, au lieu d'un canevas noir. Rien d'autre ne change. `onOverlayUpdate` est toujours appelé soixante fois par seconde. `overlayPrint` compte toujours en cellules de caractère. Vos sprites se dessinent là où ils se dessinaient avant. Un jeu avec vidéo et un jeu sans vidéo s'écrivent de la même façon, avec les mêmes fonctions, et partagent le même livre. Donc, si le jeu que vous voulez faire ne contient pas de vidéo, vous pouvez refermer cette partie maintenant et passer à la quatrième. Rien dans la quatrième partie ne dépend de quoi que ce soit dans la troisième, et les seules leçons qui ont besoin de celle-ci sont les quatre qui la suivent. Toujours là ? Bien. [#_you_already_have_a_video] === Vous avez déjà une vidéo Vous n'avez pas besoin d'en trouver une, d'en acheter une ou d'en fabriquer une. La première fois que vous avez lancé Singe, à la leçon un, un dossier appelé `Singe` est apparu dans votre dossier de travail. Ouvrez-le et regardez : parmi les fichiers que le moteur y a déballés, il y a `menuBackground.mkv`. C'est la toile de fond dont se sert le propre menu du moteur, elle fait 720 sur 480 pixels, et elle dure 420 images -- environ quatorze secondes. C'est la vidéo de cette leçon et des deux suivantes. Créez un dossier appelé `movie` dans votre dossier de travail, à côté des dossiers des autres leçons, et dedans un fichier appelé `movie.singe`. C'est toute la préparation qu'il y a à faire. Vous lancez Singe depuis le dossier de travail, comme vous le faites depuis la leçon un, donc le dossier `Singe`, vidéo comprise, est juste là à côté de votre jeu, et `Singe/menuBackground.mkv` est le nom que vous donnerez au moteur dans un instant. Il n'y a rien à copier. [#_type_this_in_2] === Tapez ceci [source,lua] ---- dofile("Singe/Framework.singe") function onOverlayUpdate() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) return OVERLAY_UPDATED end discPlay() ---- Enregistrez-le. [#_run_it_2] === Lancez-le La vidéo ne fait pas partie de votre script, donc vous ne la chargez pas dans votre script. Vous la donnez au moteur sur la ligne de commande, avec `-v` : ---- Singe -R -v Singe/menuBackground.mkv movie ---- `-v` est la forme courte de `--framefile`, et il nomme la vidéo que le jeu doit lire. Le nom de votre script vient en dernier, comme toujours. `-R` est le même `-R` que celui dont vous vous servez depuis la leçon un, et il relance toujours le jeu quand vous enregistrez. Demander, c'est tout l'intérêt de `-v`. Si vous déposez un fichier vidéo à côté de votre script sans rien dire, le moteur le remarque, le dit dans la console, et lance votre jeu sans lui. Un jeu n'est un jeu vidéo que lorsque quelqu'un le dit. Une fenêtre s'ouvre. Une boule de feu orange la remplit, un dragon se forme à partir du feu, le mot `SINGE` apparaît en dessous, puis une grille magenta s'éloigne en défilant sous un soleil orange. Dans le coin supérieur gauche, par-dessus tout ça, un nombre compte. Regardez-la jusqu'au bout. Puis regardez ce qui se passe à la fin. [#_what_just_happened_10] === Ce qui vient de se passer [source,lua] ---- discPlay() ---- Singe appelle la vidéo *le disque*, et chaque fonction qui la pilote commence par `disc`. Le nom est historique : le moteur a commencé sa vie en faisant tourner des jeux qui gardaient leur film sur un laserdisc, et le nom est resté attaché à l'idée. C'est un fichier vidéo sur votre disque dur, et rien de plus. Un jeu a au plus un disque, donc aucune des fonctions `disc` ne prend d'argument pour dire lequel. Il n'y en a qu'un. Cette ligne n'est dans aucune fonction. Elle est en bas du fichier, toute seule, et elle s'exécute une fois quand Singe charge votre script -- avant le premier `onOverlayUpdate`, avant tout le reste. Singe n'a pas de callback `onStartup` parce qu'il n'en a pas besoin : un script Lua *est* une liste d'instructions, et tout ce que vous écrivez hors d'une fonction se produit pendant la lecture du fichier. Par convention, le code de démarrage va en bas, sous les fonctions. Il vous faut `discPlay()` parce que le disque ne démarre pas tout seul. Quand le jeu commence, le moteur gare le disque sur l'image un et le met en pause là. Rien ne bouge tant que vous ne le demandez pas. [source,lua] ---- overlayPrint(2, 2, "Frame " .. discGetFrame()) ---- `discGetFrame` répond à une seule question : quelle image est affichée en ce moment ? Il ne prend pas d'argument et renvoie un nombre. Remarquez où ce nombre est dessiné. Colonne 2, ligne 2 -- exactement là où `Hello!` est allé à la leçon un, et il tombe au même endroit à l'écran. L'overlay sur lequel vous dessinez fait la moitié de la taille de la vidéo dans chaque sens, ce qui, pour une vidéo de 720 sur 480, donne 360 sur 240, c'est-à-dire exactement la taille à laquelle vous dessinez depuis treize leçons. Vos coordonnées n'ont pas bougé. Si un jour vous lisez une vidéo d'une autre taille, l'overlay change de taille avec elle, et `overlaySetResolution` le règle à la taille que vous voulez ; l'entrée de la Référence donne les détails. C'est toute l'idée de l'overlay. La vidéo est en dessous. Votre dessin est par-dessus, et partout où vous n'avez pas dessiné, le film transparaît. C'est pourquoi `overlayClear` compte encore plus maintenant qu'avant : effacez l'overlay et c'est le film que vous regardez. [#_a_frame_is_an_address] === Une image est une adresse Le nombre que renvoie `discGetFrame` est un numéro d'*image*. Une image, c'est une seule vue fixe. Cette vidéo en contient 420 et en montre environ trente par seconde, donc elle dure environ quatorze secondes. Les images sont comptées à partir de zéro, donc cette vidéo a les images 0 à 419. Le nombre d'images et le dernier numéro ne sont pas les mêmes, ce qui piège tout le monde une fois. `discGetFrameCount` vous donne le nombre -- 420 -- et l'image la plus haute que vous pouvez demander est un de moins. Ce qui compte avec un numéro d'image, c'est qu'il ne change jamais et ne dérive jamais. L'image 110 est le moment où le dragon crache du feu, dans cette copie du fichier, sur cette machine, aujourd'hui comme l'an prochain. Les secondes dérivent, parce qu'une vidéo ne se lit pas toujours exactement à la cadence que vous croyez. Un numéro d'image est l'adresse d'un moment, et les adresses sont ce dont est fait un jeu vidéo : vous notez l'image où le dragon apparaît, et à partir de là vous pouvez envoyer le film à cet endroit quand vous voulez. Trouver ces numéros est un travail que vous faites une fois par vidéo, et le script que vous avez écrit est l'outil qu'il faut : lancez le film, regardez le compteur, notez les images où il se passe quelque chose. Tout ce que contient la leçon seize repose sur une liste de numéros trouvés exactement de cette façon. [#_what_happened_at_the_end] === Ce qui s'est passé à la fin Le compteur a atteint 419, puis il est revenu à 0 et le feu était de retour. Quand le disque arrive au bout, le moteur le relance depuis l'image zéro. Il ne s'arrête pas, et il ne vous prévient pas. Un vrai jeu ne le laisse jamais arriver là : il surveille le numéro d'image et envoie le disque ailleurs avant que la fin n'arrive. Oublier de le faire, voilà pourquoi un jeu vidéo à moitié fini se met soudain à jouer sa propre séquence de titre au milieu d'un combat. [#_stopping_jumping_and_asking] === Arrêter, sauter et demander Ajoutez cette fonction au-dessus de `onOverlayUpdate` : [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() end end ---- Enregistrez, et essayez chacune. Espace lit et met en pause. Gauche revient au début. Droite saute environ une seconde plus loin. La touche `1` arrête complètement le disque, et ce que vous obtenez est un écran bleu -- la couleur qu'affichait un lecteur de laserdisc à l'arrêt, conservée parce qu'énormément de jeux dessinaient leur tableau des scores dessus. Cinq nouveaux appels, et ils se divisent en deux sortes. *Demander.* `discGetState` renvoie l'un de quatre noms que le moteur a déjà définis pour vous : `DISC_PLAYING`, `DISC_PAUSED`, `DISC_STOPPED` et `DISC_EJECTED`. Ils sont comme `OVERLAY_UPDATED` -- pas de guillemets, parce que ce sont des noms de valeurs et non du texte. Comparez avec `==`, comme vous avez comparé des nombres à la leçon trois. *Ordonner.* `discPlay` démarre ou reprend. `discPause` fige l'image affichée et la laisse à l'écran. `discStop` est le plus lourd : l'image est remplacée par l'écran bleu, `discGetFrame` répond `0` jusqu'à ce que le disque reparte, et `discPause` et les appels de saut sont ignorés tant qu'il est arrêté. N'importe lequel de `discPlay`, `discSearch` ou `discSkipToFrame` le fait repartir. Deux appels envoient le disque à une image, et la différence entre eux est la seule chose de cette leçon qui mérite d'être apprise par cœur : * `discSearch(frame)` y va et s'y *met en pause*. * `discSkipToFrame(frame)` y va et *lit* à partir de là. Search, c'est pour vous garer sur une image et attendre. Skip, c'est pour qu'une scène commence. `discSkipForward(30)` et `discSkipBackward(30)` sont la même idée, mesurée depuis l'endroit où vous êtes plutôt que depuis le début, et ils laissent le disque en lecture s'il lisait et en pause s'il était en pause. Il existe aussi `discPauseAtFrame`, qui fait exactement ce que fait `discSearch`, sous un nom plus ancien. La Référence le marque comme alias hérité. Vous le verrez dans les scripts des autres ; écrivez `discSearch` dans les vôtres. [#_waiting_is_harder_than_it_sounds] === Attendre est plus difficile qu'il n'y paraît Voici la phrase dont est fait chaque jeu vidéo : ____ Lire de l'image 0 à l'image 120, puis poser une question au joueur. ____ Essayez de l'écrire. Vous ne pouvez pas écrire « lire, puis attendre, puis demander », parce que vous n'avez nulle part où l'écrire. Vous avez `onOverlayUpdate`, que Singe appelle soixante fois par seconde et qui doit rendre la main immédiatement, à chaque fois. Une fonction qui resterait là à attendre arrêterait net le moteur : plus de dessin, plus de commandes, plus de vidéo. Donc vous ne pouvez pas dire « puis ». Vous devez transformer « puis » en une variable qui se souvient d'où vous en êtes, et la vérifier à chacun de ces soixante appels : [source,lua] ---- local asking = false function onOverlayUpdate() overlayClear() if not asking and discGetFrame() >= 120 then discPause() asking = true end if asking then overlayPrint(2, 4, "Left or right?") end return OVERLAY_UPDATED end ---- Ça marche. Lisez-le et vous voyez que ça marche. Mais remarquez ce que ça a coûté : une variable, dont le seul travail est de se souvenir de l'endroit de la phrase où vous en êtes, et deux instructions `if` pour la garder honnête. Maintenant, ajoutez le reste de la scène. Le joueur répond, donc l'une de deux choses se joue ensuite. Chacune finit quelque part et pose une autre question. Un modeste jeu à embranchements a quarante de ces moments, et chacun veut sa propre variable et son propre `if`. Vous finissez avec un script où l'histoire n'est écrite nulle part -- elle est éparpillée entre une douzaine de drapeaux, et pour savoir ce qui se passe après le dragon, il faut tous les lire. Gardez cette idée en tête. La leçon quinze n'est rien d'autre que le remède. [#_when_there_is_no_video] === Quand il n'y a pas de vidéo Relancez votre script de la façon simple, sans `-v` : ---- Singe -R movie ---- Rien ne plante. Vous obtenez une fenêtre noire avec `Frame 0` dans le coin. Ça vaut la peine de le comprendre, parce que c'est la moitié rassurante de la promesse faite au début de cette leçon. Un jeu sans disque exécute quand même chaque appel `disc` que vous faites ; ils ne font simplement rien d'utile. Le moteur documente exactement ce qu'ils répondent, et ça ne change jamais : `discGetFrame` et `discGetFrameCount` renvoient `0`, `discGetState` renvoie `DISC_EJECTED`, `discGetWidth` et `discGetHeight` renvoient la taille du canevas, et `discPlay`, `discPause`, `discSearch` et les autres sont notés dans le journal et, pour le reste, ignorés. Vous ne pouvez pas casser un script en appelant une fonction du disque quand il n'y a pas de disque. Quand un script a besoin de *savoir*, il interroge `SINGE_DISC`. C'est un autre nom que le moteur définit avant que votre script s'exécute, et il vaut `true` quand le jeu a une vidéo et `false` quand le monde est un simple canevas. C'est la bonne façon d'écrire un jeu qui peut tourner dans les deux cas : [source,lua] ---- if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end ---- `not` retourne une valeur vraie ou fausse, comme à la leçon trois. [#_the_whole_thing_7] === Le tout Mis bout à bout, avec une petite fonction qui transforme l'état en mots et une image à laquelle s'arrêter, voici le script terminé. C'est un chercheur d'images : l'outil dont vous vous servirez chaque fois que vous rencontrerez une nouvelle vidéo. [source,lua] ---- dofile("Singe/Framework.singe") local stopAt = 400 local held = false local function stateName() local state = discGetState() if state == DISC_PLAYING then return "playing" elseif state == DISC_PAUSED then return "paused" elseif state == DISC_STOPPED then return "stopped" end return "not there" end function onInputPressed(what) if what == SWITCH_BUTTON1 then if discGetState() == DISC_PLAYING then discPause() else discPlay() end elseif what == SWITCH_LEFT then discSearch(0) held = false elseif what == SWITCH_RIGHT then discSkipForward(30) elseif what == SWITCH_START1 then discStop() held = false end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() if not SINGE_DISC then overlayPrint(2, 2, "This game has no video.") overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv") return OVERLAY_UPDATED end if not held and frame >= stopAt then discPause() held = true end overlayPrint(2, 2, "Frame " .. frame .. " of " .. discGetFrameCount()) overlayPrint(2, 4, "The disc is " .. stateName() .. ".") overlayPrint(2, 6, "Space plays and pauses. Left rewinds.") overlayPrint(2, 7, "Right jumps on a second. 1 stops the disc.") if held then overlayPrint(2, 9, "Holding at frame " .. stopAt .. ".") end return OVERLAY_UPDATED end discPlay() ---- `held` est le même genre de variable que `asking` plus haut, et il est là pour une raison que vous pouvez vérifier : retirez-le, laissez `if frame >= stopAt then discPause() end`, puis essayez d'appuyer sur espace pour continuer au-delà de l'image 400. Impossible. Le disque reprend pendant un soixantième de seconde et le `onOverlayUpdate` suivant le remet en pause, pour toujours. `held`, c'est la façon de dire « j'ai déjà fait ça une fois ». [#_try_it_14] === Essayez . *Trouvez le dragon.* Lancez-le, regardez le compteur, et notez l'image où le dragon apparaît pour la première fois, l'image où il crache du feu, et l'image où la grille envahit l'écran. Vous voudrez ces trois numéros à la leçon seize. . *Avancez pas à pas.* Changez `discSkipForward(30)` en `discSkipForward(1)` et gardez la flèche droite enfoncée. Changez maintenant `stopAt` pour une image au milieu du dragon, et servez-vous de `discSearch` et de la touche fléchée pour tomber exactement dessus. . *Faites-le boucler.* Changez l'arrêt pour qu'au lieu de mettre en pause à l'image 400, il appelle `discSkipToFrame(0)`. Vous avez construit un mode attraction : une scène qui se joue sans fin jusqu'à ce que quelqu'un appuie sur un bouton. . *Demandez une image qui n'existe pas.* Changez la ligne `SWITCH_RIGHT` en `discSkipToFrame(100000)`. Ça ne plante pas. Lisez l'entrée de la Référence pour `discSkipToFrame` et trouvez la phrase qui dit pourquoi. . *Jouez-le sans la vidéo.* Lancez `Singe -R movie` puis appuyez sur toutes les touches. Rien de ce que vous pressez ne peut le casser. [#_break_it_on_purpose_13] === Cassez-le exprès L'erreur qui coûte le plus de temps ici n'est pas une erreur de Lua. Lancez ceci : ---- Singe -R -v menuBackground.mkv movie ---- Le chemin est faux -- il part du dossier de travail, et la vidéo y est dans le dossier `Singe`, pas en vrac -- et Singe refuse de démarrer. Au lieu de ça, il affiche toute sa liste d'options de ligne de commande, ce qui est alarmant la première fois, puis ceci, tout à la fin : ---- Error: Unable to locate video. ---- Un mur d'options comme celui-là veut toujours dire la même chose : le moteur n'est jamais allé jusqu'à votre jeu. Ignorez le mur et lisez la dernière ligne. Celle-là est facile. Sa version la moins utile, c'est de lancer sans aucun `-v` et d'oublier que vous l'avez fait. Il n'y a pas d'erreur, parce qu'un jeu sans vidéo est un jeu parfaitement valable. Vous obtenez un écran noir et un compteur bloqué sur `0`, et vous cherchez le bogue dans votre script pendant vingt minutes. C'est à ça que sert le bloc `SINGE_DISC` du script terminé. Trois lignes, et le jeu vous dit ce qui ne va pas au lieu de bouder. Mettez une vérification comme celle-là dans chaque script que vous écrivez qui a besoin d'une vidéo. [#_what_you_learned_14] === Ce que vous avez appris * Un jeu ne lit de la vidéo que s'il le demande, et tout le reste de Singe marche de la même façon dans les deux cas. * Singe appelle la vidéo le disque. Un jeu en a un, et les fonctions `disc` ne prennent pas de handle parce qu'il n'y a rien à choisir. * La vidéo se donne sur la ligne de commande avec `-v`, pas dans le script. * L'overlay est dessiné par-dessus l'image, et le film transparaît partout où vous n'avez pas dessiné. * Une image est une vue fixe, comptée à partir de zéro, et un numéro d'image est l'adresse d'un moment. * `discGetFrame` demande où en est le disque ; `discGetFrameCount` dit combien il y a d'images ; `discGetState` dit ce que fait le disque. * `discPlay`, `discPause` et `discStop` le pilotent. `discSearch` va à une image et se met en pause ; `discSkipToFrame` va à une image et lit. * Un disque qui arrive au bout repart de l'image zéro sans vous prévenir. * Sans disque, chaque appel `disc` est inoffensif, et `SINGE_DISC` est la façon dont un script sait dans quel genre de jeu il se trouve. * Vous ne pouvez pas attendre dans `onOverlayUpdate`, donc « puis » doit s'écrire sous la forme d'une variable. [#_next_time_14] === La prochaine fois Ce dernier point est le point sensible. Un jeu vidéo est une liste d'étapes -- lis ceci, demande cela, lis l'autre -- et vous venez de voir que le modèle à callbacks ne sait pas dire « puis » sans un drapeau et un `if` pour chaque étape. La leçon quinze vous montre l'autre façon dont Singe vous laisse écrire un script, où « puis » est une nouvelle ligne.