singe/docs/fr/lessons/15-threaded.adoc

439 lines
16 KiB
Text
Raw Permalink 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_15_the_other_way_to_write_it]
== Leçon 15 : L'autre façon de l'écrire
image::learn/15-threaded.png[La leçon terminée, 480]
Toutes les leçons jusqu'ici ont été écrites de la même façon. Vous écrivez des
fonctions dont le nom commence par `on`, Singe les appelle quand bon lui semble,
et vous ne dites jamais dans quel ordre les choses se passent. C'est le modèle
*piloté par les événements*, et c'est ainsi que fonctionnent la plupart des
moteurs de jeu.
Singe a un second modèle. Dans celui-ci, vous écrivez votre jeu comme une liste
d'étapes, de la première ligne à la dernière, et vous avez le droit d'attendre.
Dans cette leçon, vous allez écrire la même petite séquence vidéo des deux
façons et voir la différence de vos propres yeux.
[#_the_sequence]
=== La séquence
Voici ce que fait le jeu, en français :
. Démarrer le film au début et le lire jusqu'à l'image 120.
. S'arrêter là et demander au joueur d'appuyer sur la barre d'espace.
. Quand il appuie, continuer la lecture jusqu'à l'image 300.
. S'arrêter là, dire « The end. », attendre quatre secondes, et quitter.
Quatre étapes. Relisez-les et remarquez que chacune d'elles contient le mot
« puis », dit ou sous-entendu.
[#_the_way_you_know]
=== La façon que vous connaissez
Créez un fichier appelé `events.singe` dans le dossier `movie` de la leçon
quatorze, et tapez ceci :
[source,lua]
----
dofile("Singe/Framework.singe")
local step = 1
local endedAt = 0
function onInputPressed(what)
if what == SWITCH_BUTTON1 and step == 2 then
step = 3
discPlay()
end
end
function onOverlayUpdate()
local frame = discGetFrame()
overlayClear()
overlayPrint(2, 2, "Frame " .. frame)
if step == 1 then
if frame >= 120 then
discPause()
step = 2
end
elseif step == 2 then
overlayPrint(2, 4, "Press the space bar to go on.")
elseif step == 3 then
if frame >= 300 then
discPause()
endedAt = singeGetTicks()
step = 4
end
elseif step == 4 then
overlayPrint(2, 4, "The end.")
if singeGetTicks() - endedAt > 4000 then
singeQuit()
end
end
return OVERLAY_UPDATED
end
discSearch(0)
discPlay()
----
Lancez-le. Nommer le dossier lancerait `movie.singe`, alors nommez plutôt le
script, comme la leçon un vous l'a montré :
----
Singe -R -v Singe/menuBackground.mkv movie/events.singe
----
Ça marche. Ça fait exactement les quatre étapes. `singeGetTicks` est nouveau --
il renvoie le nombre de millisecondes écoulées depuis le démarrage du moteur, ce
qui est la façon ordinaire de mesurer une durée -- et `singeQuit` termine le
jeu, ce que le moteur fait pour vous quand vous appuyez sur Échap.
Maintenant, lisez le script et essayez d'y trouver les quatre étapes. Elles y
sont, mais retournées comme un gant. L'ordre de l'histoire est écrit dans les
*valeurs* de `step`, pas dans l'ordre des lignes, et `step` est modifié dans une
fonction et lu dans une autre. Pour répondre à « que se passe-t-il une fois que
le joueur a appuyé sur Espace ? », il vous faut tenir tout le script dans votre
tête d'un seul coup.
Et ceci est la version facile. Il y a deux décisions dedans.
[#_the_other_way]
=== L'autre façon
Commencez un nouveau fichier, `steps.singe`, et tapez ceci :
[source,lua]
----
local message = ""
local pressed = false
local function drawFrame()
overlayClear()
overlayPrint(2, 2, "Frame " .. discGetFrame())
overlayPrint(2, 4, message)
end
local function playTo(frame)
while discGetFrame() < frame do
drawFrame()
singeYield()
end
end
local function waitForButton()
pressed = false
while not pressed do
drawFrame()
singeYield()
end
end
local function waitSeconds(seconds)
local stopTime = singeGetTicks() + seconds * 1000
while singeGetTicks() < stopTime do
drawFrame()
singeYield()
end
end
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
pressed = true
end
end
function singeMain()
discSearch(0)
discPlay()
playTo(120)
discPause()
message = "Press the space bar to go on."
waitForButton()
message = ""
discPlay()
playTo(300)
discPause()
message = "The end."
waitSeconds(4)
end
dofile("Singe/Framework.singe")
----
Lancez-le de la même façon, en terminant par `movie/steps.singe`. Il fait les
quatre mêmes choses.
Maintenant, lisez `singeMain`. Les quatre étapes sont les quatre paragraphes,
dans l'ordre, de haut en bas, et il n'y a de variable `step` nulle part, parce
que la position dans l'histoire *est* la position dans la fonction. C'est tout
l'intérêt de cette leçon.
[#_what_just_happened_11]
=== Ce qui vient de se passer
[source,lua]
----
function singeMain()
----
`singeMain` est un nom que Singe cherche, comme `onOverlayUpdate`. Si votre
script définit cette fonction, le framework fait tourner votre jeu à partir
d'elle : elle est appelée une fois, au début, et votre jeu dure aussi longtemps qu'elle. Quand
elle se termine, le jeu s'arrête.
[source,lua]
----
singeYield()
----
Voici l'appel qui rend tout cela possible, et il veut dire « laisse passer une
image ».
Singe ne fait pas deux choses à la fois -- rien ici ne tourne en parallèle, quoi
que suggère l'expression *à fil d'exécution* (« threaded »). Ce qui se passe,
c'est que `singeYield` arrête votre fonction là où elle en est, rend la main au
moteur, et se souvient de l'endroit exact. Le moteur dessine l'image, lit les
entrées, fait avancer la vidéo, puis relance votre fonction à partir de la
ligne qui suit le `singeYield`, avec chaque variable exactement comme vous
l'avez laissée.
Une boucle `while` qui contient un `singeYield` est donc une façon d'attendre.
C'est la seule façon d'attendre, et vous devez en mettre un dans chaque boucle
qui attend quelque chose. C'est un marché que vous passez avec le moteur : vous
pouvez rester là à attendre, du moment que vous lui cédez l'image soixante fois
par seconde.
[source,lua]
----
local function playTo(frame)
while discGetFrame() < frame do
drawFrame()
singeYield()
end
end
----
Singe n'a pas de fonction « attendre que la vidéo atteigne l'image 120 ». Il
n'en a pas besoin, parce que vous pouvez l'écrire en quatre lignes, et c'est ce
que vous venez de faire. Continuez tant que le disque n'est pas arrivé à l'image
voulue ; cédez une image à chaque tour.
`waitForButton` a la même forme avec une autre question, et `waitSeconds` a
encore la même forme, avec une horloge. Trois petites fonctions, écrites une
fois, et grâce à elles `singeMain` se lit comme les phrases en français du début
de la leçon. C'est exactement ce à quoi servent vos propres fonctions, comme le
disait la leçon cinq.
[source,lua]
----
drawFrame()
singeYield()
----
Chaque boucle d'attente dessine avant de céder, et c'est ce qui surprend les
gens. Dans le modèle à fil d'exécution, vous n'avez pas de `onOverlayUpdate` à
vous, donc rien n'est dessiné si vous ne le dessinez pas. Ce que vous dessinez
avant un `singeYield` est ce qui apparaît sur cette image. Arrêtez de dessiner
et l'overlay arrête de changer.
C'est pour cela que `message` existe. `singeMain` le remplit, `drawFrame`
l'affiche, et les trois boucles d'attente appellent toutes `drawFrame`. C'est un
petit prix à payer pour remettre l'histoire dans l'ordre.
[source,lua]
----
dofile("Singe/Framework.singe")
----
En *bas*. Ceci compte plus que tout le reste de la leçon, alors ça vaut la peine
de savoir pourquoi.
`dofile` exécute un autre script à l'intérieur du vôtre, et l'une des choses que
fait le framework pendant qu'il s'exécute, c'est chercher une fonction appelée
`singeMain`. S'il en trouve une, il bâtit votre jeu autour d'elle et écrit son
propre `onOverlayUpdate`, dont le seul travail est de relancer votre `singeMain`
une fois par image. S'il n'en trouve pas, il ne fait rien de tel, et votre jeu
est un jeu ordinaire piloté par les événements.
Le framework doit donc être chargé *après* que `singeMain` existe. Chargez-le
en haut, comme vous le faites depuis la septième leçon, et au moment où il
regarde, il n'y a pas encore de `singeMain`, et vous obtenez un jeu sans jeu
dedans.
[#_the_rules]
=== Les règles
Il y en a quatre, et à elles toutes elles couvrent toutes les erreurs que vous
pouvez faire ici.
*Le framework vient en dernier.* Pour la raison ci-dessus.
*N'écrivez pas votre propre `onOverlayUpdate`.* Le framework en a écrit un, et
il ne peut y en avoir qu'un : celui des deux qui est écrit en dernier gagne, et
l'autre est jeté sans un mot. Au-dessus du `dofile`, c'est le vôtre qui saute et
votre dessin n'apparaît jamais. En dessous, c'est celui du framework qui saute
et votre `singeMain` ne tourne plus jamais. Dessinez plutôt depuis l'intérieur
de `singeMain`.
*Chaque boucle qui attend a besoin d'un `singeYield`.* Une boucle sans lui ne
cède jamais l'image. La fenêtre arrête de se redessiner, arrête de répondre, et
le jeu est perdu ; vous fermez le terminal pour en sortir. Il n'y a pas de
message d'erreur, parce que du point de vue du moteur, votre fonction n'a pas
encore fini.
*Quand `singeMain` se termine, le jeu s'arrête.* Ce qui est propre quand vous le
voulez, et une surprise quand vous ne le voulez pas. Un jeu qui doit continuer
pour toujours se termine par une boucle dont il ne sort jamais :
[source,lua]
----
while true do
drawFrame()
singeYield()
end
----
[#_input_still_arrives]
=== Les entrées arrivent toujours
Regardez de nouveau l'endroit où `pressed` reçoit sa valeur :
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
pressed = true
end
end
----
C'est un callback, dans un script écrit comme une liste d'étapes. Les deux
modèles tournent en même temps, et c'est voulu.
Quand `singeMain` existe, seul `onOverlayUpdate` vous est retiré. Tout le reste
-- `onInputPressed`, `onInputReleased`, `onKeyPressed`, `onSoundCompleted`,
`onShutdown`, tous -- se déclenche exactement comme avant, dans les intervalles
où vous avez cédé la main. La façon habituelle d'écrire un jeu dans ce modèle
est donc celle que vous venez d'employer : l'histoire dans `singeMain`, et un
petit callback qui attrape un événement et laisse un mot que l'histoire
trouvera.
`waitForButton` met `pressed` à `false` avant de commencer à attendre, ce qui
jette tout ce que le joueur a martelé avant. Oubliez cette ligne et un bouton
enfoncé pendant la vidéo compte comme la réponse à une question qui n'a pas
encore été posée.
[#_which_one_to_use]
=== Laquelle utiliser
Aucun des deux modèles n'est le meilleur. Ils conviennent à des tâches
différentes, et un vrai jeu prend celui qui va.
Le modèle à fil d'exécution convient à tout ce qui est une *séquence*. Lire
ceci, puis demander, puis lire cela. Une introduction. Une cinématique. Un
tutoriel. Un combat de boss en trois phases. Tout ce que vous décririez
naturellement comme une liste numérotée est un `singeMain` qui attend d'être
écrit, et la vidéo est la chose la plus séquentielle qui soit, c'est pourquoi
la troisième partie y arrive ici.
Le modèle piloté par les événements convient à tout ce qui est une *situation*.
Dix ennemis et un joueur, qui bougent tous, se heurtent tous, sans aucun ordre.
Le jeu que vous avez écrit à la leçon sept ne contient aucune séquence : chaque
image est la même question posée à nouveau, ce qui est exactement la forme
de `onOverlayUpdate`. Écrivez-le comme une liste d'étapes et vous ne gagnez rien
et perdez la clarté.
Quand un jeu est les deux -- et c'est le cas de la plupart -- il s'écrit des deux
façons. `singeMain` parcourt les niveaux dans l'ordre, et dans chaque niveau,
une boucle fait le travail de la leçon sept soixante fois par seconde jusqu'à la
fin du niveau.
[#_try_it_15]
=== Essayez
. *Ajoutez une étape.* Après `waitForButton`, faites plutôt sauter le film en
arrière : `discSkipToFrame(0)`, puis `playTo(60)`, puis continuez comme avant.
Deux nouvelles lignes à un seul endroit, et rien d'autre dans le script n'a
besoin de le savoir.
. *Ajoutez la même étape à l'autre.* Faites-le maintenant dans `events.singe`.
Comptez combien d'endroits vous avez dû toucher.
. *Écrivez `waitFrames`.* Une quatrième fonction utilitaire, à côté des trois
autres, qui attend un nombre donné d'images de vidéo plutôt qu'un nombre donné
de secondes. Utilisez `discGetFrame` et retenez d'où vous êtes parti.
. *Ne cédez plus la main.* Supprimez le `singeYield()` à l'intérieur de
`playTo` et lancez. Soyez prêt à fermer le terminal, parce que rien d'autre ne l'arrête. Puis remettez-le et
supprimez plutôt `drawFrame()`, et déduisez de ce que vous voyez pourquoi le
compteur d'images s'arrête mais pas le film.
. *Faites-en une boucle.* Mettez tout le contenu de `singeMain` dans
`while true do ... end` pour que la séquence recommence au lieu de quitter.
Échap vous permet toujours de sortir, parce que ça n'a jamais été votre
travail.
[#_break_it_on_purpose_14]
=== Cassez-le exprès
Déplacez la ligne `dofile("Singe/Framework.singe")` du bas de `steps.singe`
vers le haut, et lancez.
Il n'y a pas d'erreur. Rien n'est affiché. La fenêtre s'ouvre, la première image
du film reste là, et c'est tout ce qui se passera jamais.
C'est la pire sorte de bug, et c'est pour ça qu'il vaut la peine de le
rencontrer exprès. Rien ne s'est mal passé, du point de vue du moteur : votre
script s'est chargé, le framework s'est chargé, et aucun des deux n'a défini
de `onOverlayUpdate`, donc le moteur n'a rien à appeler. Le disque est garé sur
l'image un, en pause, exactement comme le moteur l'a laissé, parce que le
`discPlay` qui l'aurait démarré est à l'intérieur d'un `singeMain` que personne
ne lance jamais.
Quand un jeu à fil d'exécution ne fait rien du tout, regardez d'abord la
dernière ligne du fichier. C'est presque toujours ça.
[#_what_you_learned_15]
=== Ce que vous avez appris
* Singe a deux façons d'écrire un script, et vous choisissez en définissant
`singeMain` ou non.
* Dans le modèle à fil d'exécution, votre jeu est une liste d'étapes et
s'exécute de haut en bas.
* `singeYield()` cède une image au moteur et reprend à la ligne suivante.
* Attendre, c'est une boucle `while` avec un `singeYield` dedans. Vous écrivez
la fonction d'attente dont vous avez besoin ; le moteur n'en fournit pas.
* Rien n'est dessiné si vous ne le dessinez pas, avant de céder, à chaque
image.
* `dofile("Singe/Framework.singe")` va à la *fin* d'un script à fil
d'exécution.
* N'écrivez pas votre propre `onOverlayUpdate` quand `singeMain` existe.
* Tous les autres callbacks se déclenchent toujours, donc événements et étapes
se mélangent librement.
* Quand `singeMain` se termine, le jeu s'arrête.
* Les séquences veulent le modèle à fil d'exécution ; les situations veulent le
modèle piloté par les événements.
[#_next_time_15]
=== La prochaine fois
Vous savez maintenant écrire « lire ceci, puis demander, puis lire cela » en
trois lignes. Un jeu vidéo à embranchements n'est rien d'autre que cette
phrase, quelques centaines de fois, avec les réponses qui décident quelle ligne
vient ensuite. La leçon seize en construit un, et la partie intéressante se
révèle ne pas être la vidéo du tout. C'est de garder la carte de ce qui mène où
sous une forme que vous pouvez encore lire quand le jeu compte quarante scènes.