singe/docs/fr/lessons/14-video.adoc

506 lines
20 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_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.