singe/docs/fr/lessons/20-gui.adoc

640 lines
24 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_20_menus_and_screens]
== Leçon 20 : Menus et écrans
image::learn/20-gui.png[La leçon terminée, 480]
Vous pouvez construire un menu avec ce que vous savez déjà. Une boîte, un peu
de texte, un curseur qui bouge quand le stick bouge, un `if` pour chaque choix,
et de l'arithmétique pour garder tout ça aligné quand vous ajoutez une
quatrième option. Ça marche. C'est aussi une centaine de lignes pour une page
avec trois boutons, et chacune de ces lignes, c'est à vous de l'entretenir.
Singe contient tout un moteur de documents -- RmlUi -- et un menu écrit pour lui
est une page, comme une page web, avec une feuille de style. Elle fait passer
son texte à la ligne toute seule. Elle se met en page d'elle-même quand vous
ajoutez une option. Elle sait ce qu'est un bouton, ce qu'est un curseur, et
quel contrôle le joueur est en train de viser. Cette leçon s'en sert pour
construire un menu de pause, par-dessus un jeu qui continue de tourner
derrière.
[#_two_files]
=== Deux fichiers
Une page de GUI, c'est deux choses : un *document* qui dit ce qu'il y a sur la
page, et du *Lua* qui dit ce que fait la page. Ils vivent dans des fichiers
séparés, et c'est tout l'intérêt de l'exercice -- la mise en page cesse d'être
du code.
Commencez par un script appelé `menu.singe` et un jeu si petit qu'il tient en
un paragraphe : un vaisseau qui traverse l'écran, et un score qui augmente
chaque fois qu'il arrive de l'autre côté. Vous écrivez ce genre de chose depuis
la leçon sept.
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
local ship = spriteLoad(DIR .. "art/spaceship.png")
local shipX = 0
local speed = 2
local score = 0
function onOverlayUpdate()
overlayClear()
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
spriteDraw(ship, shipX, 300)
return OVERLAY_UPDATED
end
----
Ça tourne. Maintenant, le document. À côté de votre script, créez un fichier
appelé `pause.rml` :
[source,html]
----
<rml>
<head>
<title>Pause</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
#frame { width: 380dp; margin: 80dp auto; }
.row { display: block; margin: 8dp 0; }
label { display: inline-block; width: 100dp; }
button { nav: auto; }
input.range { nav: auto; width: 200dp; }
#status { min-height: 28dp; }
</style>
</head>
<body id="pause">
<div id="frame" class="panel">
<h1>Paused</h1>
<p class="row">Score <span id="score">000000</span></p>
<div class="row"><label>Speed</label><input type="range" id="speed" min="1" max="8" step="1" value="2"/></div>
<div class="row">
<button id="resume" autofocus>Resume</button>
<button id="restart">Restart</button>
<button id="quit">Quit</button>
</div>
<p id="status" class="muted">Arrow keys move, Return chooses.</p>
</div>
</body>
</rml>
----
Si vous avez déjà vu le code source d'une page web, ça vous dira quelque chose.
Sinon, voici tout ce qu'il vous faut pour le lire.
Un document est fait d'*éléments*. Un élément commence par un nom entre
chevrons, `<h1>`, finit par le même nom avec une barre oblique, `</h1>`, et
tout ce qui se trouve entre les deux est à l'intérieur. Certains éléments n'ont
rien à l'intérieur et se ferment eux-mêmes avec une barre oblique à la fin,
comme le `<input .../>` ci-dessus. Les éléments s'imbriquent, et l'indentation
montre l'imbrication de la même façon qu'en Lua.
Les mots à l'intérieur de la balise ouvrante sont des *attributs*, écrits
`name="value"`. Deux d'entre eux vous concernent plus que les autres. `id`
donne à un élément un nom grâce auquel votre script peut le trouver, et c'est
ainsi que Lua et le document se parlent. `class` met l'élément dans un groupe
que la feuille de style peut viser ; `panel`, `muted` et `list` sont des
groupes que la feuille de style du moteur connaît déjà.
Le langage de balisage s'appelle RML et le langage de style s'appelle RCSS. Ce
sont ceux de RmlUi, pas ceux de Singe. Le chapitre GUI de la Référence renvoie à
la documentation de RmlUi pour la liste complète de ce que vous pouvez écrire,
et vous en aurez besoin un jour. Tout ce qui est dans cette leçon fonctionne sans.
[#_show_it]
=== Affichez-la
Retour au script. Au-dessus de `onOverlayUpdate`, ajoutez :
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local page = guiLoad(gui, DIR .. "pause.rml")
----
`guiNew` crée une *GUI* : un rectangle de taille fixe dans lequel des documents
sont mis en page et dessinés. L'appel renvoie un handle, comme le font
`fontLoad` et `spriteLoad`. La faire de la taille de l'overlay veut dire qu'un
pixel du document est un pixel de votre overlay, ce qui garde simple
l'arithmétique que vous faites de tête.
`guiLoad` lit un document dans cette GUI et renvoie un second handle, pour le
document lui-même. Presque tous les autres appels prennent les deux : la GUI et
le document qu'elle contient. Une GUI peut contenir jusqu'à trente-deux
documents -- une page de titre, une page d'options et une page de fin de partie
peuvent en partager une -- et il peut exister jusqu'à seize GUI à la fois.
Rien n'est encore à l'écran, parce que charger un document ne le dessine pas.
Ajoutez une ligne à `onOverlayUpdate`, juste avant le `return` :
[source,lua]
----
guiDraw(gui)
----
Lancez-le. Le panneau est là, par-dessus le vaisseau, avec son titre, ses trois
boutons et son curseur, et vous n'avez écrit aucun code de dessin.
`guiDraw` superpose la GUI à l'overlay *pour cette image seulement*, comme tous
les autres appels de dessin, donc sa place est dans `onOverlayUpdate` et il
faut l'appeler de nouveau à l'image suivante. L'endroit où vous la mettez
décide de ce qui est au-dessus : tout ce que vous avez dessiné avant est en
dessous, et tout ce que vous dessinez après passe par-dessus. Un réticule
dessiné après `guiDraw` n'est jamais caché par le menu.
[#_let_the_player_use_it]
=== Laissez le joueur s'en servir
Cliquez sur un bouton. Il ne se passe rien, et le bouton ne s'allume même pas.
Une nouvelle GUI est une image, pas un tableau de commandes. C'est voulu : un
HUD ou une pancarte ne doit pas avaler le bouton de tir. Pour qu'une page
accepte les entrées :
[source,lua]
----
guiSetInput(gui, true)
----
Maintenant la souris fonctionne, les boutons s'allument sous le pointeur, et
les touches fléchées passent d'un contrôle à l'autre, parce que le document l'a
demandé avec `nav: auto`. Sur une manette ou un stick, les quatre directions
arrivent comme les touches fléchées, le premier bouton d'action arrive comme
Entrée, et le second comme Échap. Une borne avec un joystick et deux boutons
pilote cette page sans savoir que c'est un document.
On dit que le contrôle sur lequel agissent les touches a le *focus*, et un
bouton qui a le focus porte une bordure dorée. Le focus démarre sur Resume parce
que ce bouton porte `autofocus` : chaque fois que la page s'affiche, le focus va
au contrôle marqué ainsi. Retirez-le et les touches fléchées n'ont aucun point
de départ tant que le joueur n'a pas cliqué quelque part, ce qui, sur une borne
sans souris, n'arrive jamais.
Pourtant, cliquer ne fait toujours rien, parce que personne n'écoute.
[#_talk_to_it]
=== Parlez-lui
Trois appels relient le document à votre script, et à eux trois ils font
presque tout.
[source,lua]
----
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
----
`guiSetHandler` dit : quand l'élément qui porte cet `id` fait telle chose,
appelle cette fonction. Cet `id` est celui du document. Le nom de l'événement est
celui de RmlUi : `"click"` pour les boutons, `"change"` pour tout ce que le
joueur règle, et une longue liste d'autres que vous pourrez chercher quand vous
en aurez besoin. Une seule fonction est gardée par élément et par événement,
donc en régler une autre la remplace, et passer `nil` au lieu d'une fonction la
retire.
Votre fonction est appelée avec cinq arguments -- la GUI, le document, l'id,
l'événement, et la valeur actuelle de l'élément sous forme de chaîne.
Prenez-en autant que vous voulez et ignorez le reste, comme vous ignorez déjà
des arguments en Lua.
[source,lua]
----
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
end)
----
Le curseur transmet sa valeur sous forme de chaîne, et RmlUi formate les
nombres à sa façon, donc `4` arrive sous la forme `"4.000000"`. `tonumber`
transforme la chaîne en nombre et `math.floor` jette la partie décimale. Sauter
cette étape et utiliser la chaîne comme un nombre est une erreur que vous ferez
une fois.
[source,lua]
----
guiSetValue(gui, page, "score", string.format("%06d", score))
----
`guiSetValue` écrit dans un élément. Pour un contrôle de formulaire, elle règle
la valeur du contrôle ; pour tout le reste -- un paragraphe, une `div`, le
`<span id="score">` de votre document -- elle remplace ce qu'il y a dans
l'élément, et la page refait sa mise en page à l'image suivante. `guiGetValue`
est le même appel à l'envers, et elle répond `nil` pour un id que rien ne
porte, si bien que vous pouvez demander des nouvelles d'un élément qui n'est
peut-être pas là sans que tout plante.
Voilà toute l'API de tous les jours : `guiSetHandler`, `guiSetValue`,
`guiGetValue`. Il y a bien plus à disposition à travers les objets Lua propres
à RmlUi, accessibles par la globale `rmlui`, et le chapitre GUI de la référence
montre comment. Vous irez loin avant d'en avoir besoin.
[#_hide_it_again]
=== Cachez-la de nouveau
Un menu de pause n'est pas affiché tout le temps. `guiHide` retire un document
de l'écran et `guiShow` le remet, en gardant tout ce que le joueur a tapé ou
choisi.
La pause elle-même, c'est `singeSetPauseFlag(true)`, qui arrête le disque, les
vidéos et les sons mais *continue d'appeler vos callbacks*, si bien que vous
pouvez encore dessiner. Le moteur a sa propre touche de pause, qui fige
complètement le script, et les deux se battraient ;
`singeSetPauseKeyEnabled(false)` retire cette touche au moteur pour que
`SWITCH_PAUSE` arrive à `onInputPressed` comme n'importe quel autre contact.
Sur un clavier, cette touche est *P*.
[source,lua]
----
local function closeMenu()
paused = false
singeSetPauseFlag(false)
guiHide(gui, page)
guiSetInput(gui, false)
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
----
Couper les entrées en même temps que la page n'est pas de la décoration. Une
page cachée qui accepte encore les entrées mange les touches fléchées du joueur
pour le reste de la partie.
[#_what_the_page_does_not_use_you_still_get]
=== Ce que la page n'utilise pas, vous le recevez quand même
C'est la règle qui permet de laisser une GUI affichée pendant le jeu sans
danger, et elle mérite d'être dite clairement : *un événement qu'un élément a
utilisé n'atteint jamais vos callbacks.* Un appui sur Entrée qui a actionné le
bouton qui a le focus n'arrive pas comme `onInputPressed`. Une flèche qui a
déplacé le focus n'arrive pas comme `SWITCH_LEFT`. Un clic sur un bouton n'est
pas un contact de souris.
Tout ce que la page n'a pas utilisé passe tout droit, comme d'habitude. C'est
pour ça que `P` ferme ce menu : aucun élément du document n'utilise `P`, donc
la touche arrive à `onInputPressed` exactement comme quand le menu n'est pas
affiché. C'est aussi pour ça qu'un HUD qui accepte les entrées laisse quand
même passer le bouton de tir.
Une touche ne suit pas la règle. RmlUi utilise Tab pour déplacer le focus et ne
rend jamais la touche, donc Singe ne donne Tab à une GUI que pendant qu'on est
réellement en train de taper dans un champ de texte. Un jeu qui affecte Tab à
quelque chose le garde.
[#_style_it]
=== Mettez-la en forme
Revenez au `<head>` du document. Une ligne fait l'essentiel du travail :
[source,html]
----
<link type="text/rcss" href="Singe/gui.rcss"/>
----
C'est le thème que le moteur fournit, et c'est pour ça que votre panneau a une
bordure, un dégradé et un titre doré sans que vous ayez rien choisi. Sans
feuille de style, RmlUi ne dessine presque rien, parce que chaque élément part
en ligne et à la taille de son contenu ; alors lier celle-ci en premier et
redéfinir ce qui vous déplaît est le chemin le plus rapide vers une page qui
ressemble à quelque chose.
Le thème vous donne `.panel` (une boîte arrondie avec une bordure), `.list`
(une boîte qui défile et dont les lignes `div` enfants sont mises en évidence
et prennent le focus), `.muted` (un texte plus pâle pour les indications), des
titres, et tous les contrôles de formulaire avec leurs états survol, focus et
enfoncé. Le chapitre GUI de la Référence les liste.
Tout ce qui vient après le lien est à vous. Le bloc `<style>` de `pause.rml`
redéfinit le thème pour cette page, les règles qui viennent après l'emportant
sur celles d'avant, et une règle qui vise un `id` bat une règle qui vise une
`class`. Pour un vrai jeu, mettez vos propres règles dans un fichier `.rcss` à
côté du document et liez-le en second, pour que votre style tienne en un seul
fichier pour toutes les pages.
L'unité `dp` est un pixel de la GUI, à sa propre taille -- votre GUI fait la
taille de l'overlay, donc `380dp` font 380 pixels de l'overlay.
`margin: 80dp auto` est l'astuce qui centre un bloc de largeur connue : `auto`
partage à égalité entre les deux côtés tout ce qui reste. Il n'y a rien à
mesurer ni à calculer, ce qui, après la leçon dix-neuf, vous agacera peut-être
un peu.
Pour utiliser votre propre police dans un document, chargez-la avec
`guiLoadFont` avant les documents qui s'en servent, et nommez-la en RCSS par le
nom de famille inscrit dans le fichier de police, pas par son nom de fichier.
[#_when_not_to_use_any_of_this]
=== Quand ne rien utiliser de tout ça
Une GUI est une page. C'est la bonne réponse pour un menu, un écran d'options,
un tableau des meilleurs scores, un écran de fin de partie, ou un HUD avec une
vraie mise en page.
C'est la mauvaise réponse pour un score dans le coin. Ça, c'est deux lignes de
`fontPrint` de la leçon dix-neuf, et l'emballer dans un document ne vous
rapporte rien et vous coûte une texture. Il en va de même pour un réticule, une
barre de vie que vous avez dessinée avec `overlayBox`, et une ligne de texte de
débogage.
Trois faits tranchent pour vous.
Une GUI a besoin d'un GPU. Sur une machine qui n'en a pas -- un vieux PC, un
petit Pi, une machine sans aucun pilote Vulkan, Direct3D 12 ou Metal -- il n'y
a pas de GUI, et `guiNew` ne répond pas poliment `false`, elle met fin à votre
jeu. La scène 3D de la leçon vingt-deux suit le même chemin, donc ce n'est pas
la dernière fois que vous poserez la question. Demandez d'abord
`singeHasGpu()` si votre jeu doit tourner aux deux endroits, et prévoyez une
version simple à l'overlay sur laquelle vous rabattre. Le menu du moteur
lui-même fait exactement ça.
Une GUI coûte une texture et une passe de mise en page *à chaque image, que
vous la dessiniez ou non*. Une GUI dont vous avez fini de vous servir doit
être supprimée avec `guiDelete`, pas simplement cachée. Cacher, c'est pour une
page que vous montrerez de nouveau dans une minute.
Et une GUI est détruite par un rechargement. `F5`, `-R` et tout ce qui relance
le script emportent toutes les GUI avec eux, et le script les refait quand il
s'exécute. Ce n'est pas grave, tant que vous les construisez dans le corps de
votre script et pas dans un coin qui ne s'exécute qu'une fois.
[#_the_whole_script_2]
=== Le script complet
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
singeSetPauseKeyEnabled(false)
local SHIP_Y = 300
local ship = spriteLoad(DIR .. "art/spaceship.png")
local hudFont = fontLoad("Singe/FreeSansBold.ttf", 20)
local shipX = 0
local speed = 2
local score = 0
local paused = false
local gui = nil
local page = nil
local function say(text)
if gui ~= nil then
guiSetValue(gui, page, "status", text)
end
end
local function closeMenu()
paused = false
singeSetPauseFlag(false)
if gui ~= nil then
guiHide(gui, page)
guiSetInput(gui, false)
end
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
if gui ~= nil then
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
end
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
guiHide(gui, page)
guiSetInput(gui, false)
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
guiSetHandler(gui, page, "quit", "click", function()
singeQuit()
end)
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
say("Speed " .. speed)
end)
end
function onInputPressed(what)
if what == SWITCH_PAUSE then
if paused then
closeMenu()
else
openMenu()
end
end
end
function onOverlayUpdate()
overlayClear()
if not paused then
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
end
spriteDraw(ship, shipX, SHIP_Y)
fontSelect(hudFont)
colorForeground(255, 255, 255)
fontPrint(16, 16, string.format("SCORE %06d", score))
fontPrint(16, 40, "P pauses")
if paused then
if gui ~= nil then
guiDraw(gui)
else
colorForeground(255, 211, 90)
fontPrint(16, 70, "PAUSED -- no GPU, so no menu")
end
end
return OVERLAY_UPDATED
end
function onShutdown()
if gui ~= nil then
guiDelete(gui)
end
spriteUnload(ship)
fontUnload(hudFont)
end
----
[#_what_just_happened_16]
=== Ce qui vient de se passer
[source,lua]
----
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
----
Toute la GUI est construite à l'intérieur de ce `if`, et `gui` reste `nil` sur
une machine sans GPU. Chaque appel qui touche à la GUI est protégé par
`if gui ~= nil`, et la pause marche toujours sans menu. Ça fait quatre lignes de
plus, et c'est la différence entre « n'a pas de menu sur cette machine » et
« ne démarre pas sur cette machine ».
[source,lua]
----
guiHide(gui, page)
guiSetInput(gui, false)
----
`guiLoad` affiche le document qu'elle a chargé, donc une page qui doit démarrer
cachée doit être cachée immédiatement. Les entrées sont déjà coupées pour une
nouvelle GUI, mais les couper ici fait de `openMenu` et `closeMenu` le reflet
exact l'une de l'autre, et des paires en miroir sont bien plus difficiles à
rater.
[source,lua]
----
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
----
Le gestionnaire est une fonction sans nom, écrite là où on s'en sert. Lua vous
laisse écrire `function(...) ... end` partout où une valeur est attendue, et
puisque `guiSetHandler` attend une fonction, vous pouvez lui en donner une
sur-le-champ au lieu de la nommer d'abord et de passer le nom. Elle atteint
`score`, `shipX` et `closeMenu` parce qu'ils sont déclarés au-dessus d'elle
dans le même fichier, et c'est toute la raison pour laquelle les gestionnaires
sont mis en place sous les choses qu'ils touchent.
[source,lua]
----
if not paused then
shipX = shipX + speed
----
Le drapeau de pause arrête le disque et les sons, pas votre arithmétique.
`onOverlayUpdate` continue d'être appelée -- c'est tout l'intérêt, sinon le
menu ne pourrait pas être dessiné -- donc tout ce qui est à vous et qui doit
s'arrêter pendant la pause, c'est à vous de l'arrêter.
[#_try_it_20]
=== Essayez
. *Ajoutez un quatrième bouton.* Copiez la ligne `<button id="quit">`,
donnez-lui un nouvel id et un nouveau texte, et ajoutez-lui un gestionnaire.
Remarquez que rien n'a eu besoin d'être remesuré ni décalé.
. *Changez son style.* Dans le bloc `<style>`, changez `#frame` en
`width: 260dp; margin: 20dp;` et regardez le panneau se déplacer et le texte
se remettre à la ligne tout seul.
. *Retirez le `guiSetInput(gui, true)` de `openMenu`.* La page apparaît
toujours. Trouvez, rien qu'en regardant l'écran, ce que vous avez perdu.
. *Laissez la page affichée tout le temps.* Supprimez les appels à `guiHide`,
changez les deux appels `guiSetInput(gui, false)` en `true`, et dessinez la
GUI à chaque image au lieu de seulement pendant la pause. Puis appuyez sur les
touches fléchées et sur Entrée pendant que le vaisseau vole. La page les prend
pour elle : le focus se déplace, le curseur change la vitesse, et Entrée
appuie sur le bouton qui a le focus. Un jeu qui se piloterait avec ces touches
ne les entendrait jamais. Voilà pourquoi un HUD, c'est
`guiSetInput(gui, false)`.
. *Mettez le score sur la page plutôt que dans le HUD.* Appelez `guiSetValue`
pour `score` à chaque image plutôt que seulement dans `openMenu`, et
supprimez la ligne `fontPrint`. Puis décidez laquelle des deux vous préférez
vraiment, et pourquoi.
[#_break_it_on_purpose_19]
=== Cassez-le exprès
Changez un id dans `pause.rml`, de `id="resume"` en `id="continue"`, et ne
touchez pas au script. Singe démarre et plante :
----
52:guiSetHandler: No element "resume" in document 0 of GUI 0.
----
Lisez-le comme n'importe quelle erreur du moteur. La ligne, l'appel qui s'est
plaint, puis la plainte -- et cette plainte est précise. Elle ne dit pas que le
document est cassé ; le document s'est chargé parfaitement. Elle dit que vous
avez demandé un élément par un nom que rien en lui ne porte.
`guiSetHandler` et `guiSetValue` plantent toutes les deux sur un id manquant,
parce qu'un gestionnaire que vous vouliez attacher et que vous n'avez pas
attaché est un bug dont vous voulez entendre parler tout de suite.
`guiGetValue` est l'exception : elle répond `nil` à la place, pour que vous
puissiez demander si quelque chose est là.
L'autre erreur qui vaut d'être rencontrée est celle que vous obtenez quand le
document lui-même ne se charge pas. Renommez `pause.rml` et lancez. Cette fois,
Singe nomme le fichier et cite la raison donnée par RmlUi, et les ronchonnements
de RmlUi à propos de votre balisage et de vos styles vont à la console, que le
chargement réussisse ou non. Lisez cette console la première fois qu'une page
sort vide ; elle aura en général dit pourquoi.
[#_what_you_learned_20]
=== Ce que vous avez appris
* Une GUI est un rectangle de taille fixe dans lequel des documents sont mis en
page et dessinés ; `guiNew` en crée une et `guiDelete` la libère.
* `guiLoad` met un document RML dans une GUI et l'affiche ; `guiHide`,
`guiShow` et `guiClose` le gèrent ensuite.
* `guiDraw` superpose la GUI à l'overlay pour une image, donc sa place est dans
`onOverlayUpdate` comme tous les autres appels de dessin.
* Une nouvelle GUI ne fait qu'afficher ; c'est `guiSetInput` qui la rend
utilisable.
* `guiSetHandler` exécute votre fonction quand on clique sur un élément ou
qu'on le modifie, `guiSetValue` écrit dans un élément, et `guiGetValue` en lit
un.
* Un événement qu'un élément a utilisé n'atteint jamais vos propres callbacks ;
tout le reste passe comme d'habitude.
* `Singe/gui.rcss` est le thème fourni ; liez-le en premier et redéfinissez-le
dans un bloc `<style>` ou dans une feuille à vous.
* Une GUI a besoin d'un GPU, alors demandez `singeHasGpu()` si votre jeu doit
tourner sans.
* Chaque GUI fait son rendu à chaque image, que vous la dessiniez ou non, alors
supprimez celles dont vous avez fini de vous servir.
* Un score dans le coin, c'est deux lignes de `fontPrint`, pas un document.
[#_next_time_20]
=== La prochaine fois
Il reste une autre sorte de texte à l'écran, et elle a son propre système pour
une raison qui n'a rien à voir avec son apparence. La leçon vingt et un parle
des sous-titres, et des joueurs qui ne peuvent pas jouer à votre jeu sans eux.