439 lines
16 KiB
Text
439 lines
16 KiB
Text
[#_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.
|