640 lines
24 KiB
Text
640 lines
24 KiB
Text
[#_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.
|