singe/docs/fr/lessons/19-text.adoc

566 lines
22 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_19_text_that_looks_good]
== Leçon 19 : Du texte qui a de l'allure
image::learn/19-text.png[La leçon terminée, 480]
Chaque mot que vous avez mis à l'écran depuis la leçon un est sorti de
`overlayPrint`, et `overlayPrint` a exactement une police : un bloc de pixels à
chasse fixe intégré au moteur, toujours de la même taille, dans la couleur qu'il
se trouve avoir. C'est parfait pour un compteur d'images. C'est mauvais pour un
titre, un score, un menu, ou tout ce qu'un joueur est censé regarder plutôt que
survoler.
Cette leçon charge une vraie police depuis le disque, affiche du texte avec, le
colore, le mesure pour que vous puissiez centrer les choses, et vous montre ce
que coûte vraiment l'affichage de texte. À la fin, vous aurez un écran titre
qui a l'air voulu plutôt qu'accidentel.
[#_set_the_overlay_to_a_size_you_can_think_in]
=== Donnez à l'overlay une taille qui vous parle
Commencez un nouveau fichier appelé `text.singe` avec deux lignes :
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
----
Vous avez écrit la première ligne de nombreuses fois ; elle charge le
framework, qui définit `DIR`, les noms `SWITCH_*`, et les noms `FONT_QUALITY_*`
dont vous allez vous servir.
La seconde compte plus qu'elle n'en a l'air. L'overlay -- la surface sur
laquelle atterrit tout ce que vous dessinez -- commence à la *moitié* de la
taille du monde. Un jeu sans vidéo a un canevas de 720 sur 480, donc l'overlay
commence à 360 sur 240, et une police de 36 points sur un overlay de 360 pixels
de large est énorme. `overlaySetResolution` remplace l'overlay par un overlay de
la taille que vous demandez, et à partir de là, chaque coordonnée que vous
donnez, et tout ce que vous disent `overlayGetWidth` et `overlayGetHeight`, est
dans cet espace.
`discGetWidth()` et `discGetHeight()` donnent la taille de la vidéo quand il y
en a une et la taille du canevas quand il n'y en a pas, donc cette seule ligne
vous donne un overlay accordé au monde, quel que soit le genre de jeu.
Appelez-la une fois, au début, avant de dessiner quoi que ce soit.
[#_load_a_font]
=== Chargez une police
En dessous, ajoutez :
[source,lua]
----
fontQuality(FONT_QUALITY_BLENDED)
local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
----
`fontLoad` ouvre un fichier de police TrueType et vous renvoie un nombre -- un
*handle* -- dont vous vous servez chaque fois que vous voulez cette police. Le
second argument est la taille en points, et elle est fixée pour toute la vie du
handle. Il n'y a aucun moyen de changer ensuite la taille d'une police chargée.
Si vous voulez la même police en trois tailles, vous appelez `fontLoad` trois
fois et vous gardez trois handles.
`Singe/FreeSansBold.ttf` est la police que le moteur a déballée dans le dossier
`Singe` de votre dossier de travail la première fois que vous avez lancé quoi
que ce soit. Chaque copie de Singe l'a, donc vous pouvez compter dessus.
N'importe quel autre fichier `.ttf` marche de la même façon ; mettez-le à côté
de votre script et chargez-le avec `DIR .. "myfont.ttf"`.
Maintenant, affichez du texte avec :
[source,lua]
----
function onOverlayUpdate()
overlayClear()
fontPrint(24, 60, "ASTEROID PATROL")
return OVERLAY_UPDATED
end
----
Lancez-le. De grandes lettres blanches près du coin supérieur gauche.
Remarquez ce que vous n'avez *pas* fait : vous n'avez jamais dit à `fontPrint`
quelle police utiliser. `fontLoad` fait de la police qu'il vient de charger la
police sélectionnée, et `fontPrint` dessine toujours avec celle qui est
sélectionnée. Avec plus d'une police chargée, vous choisissez entre elles avec
`fontSelect`, ce que vous allez faire dans un instant.
Remarquez aussi que `24` et `60` sont des pixels, pas des cellules de caractère.
C'est la différence entre `fontPrint` et `overlayPrint` qui fait trébucher les
gens : `overlayPrint(2, 2, ...)` veut dire deux lettres vers la droite et deux
lignes vers le bas, alors que `fontPrint(2, 2, ...)` veut dire deux pixels vers
la droite et deux pixels vers le bas, c'est-à-dire coincé dans le coin. Le `y`
que vous donnez est le *haut* du texte, pas la ligne de base sur laquelle il
repose.
[#_color]
=== La couleur
`fontPrint` prend trois arguments et aucun n'est une couleur. La couleur vient
d'ailleurs :
[source,lua]
----
colorForeground(255, 211, 90)
fontPrint(24, 60, "ASTEROID PATROL")
----
`colorForeground` règle le rouge, le vert et le bleu de `0` à `255`, avec un
quatrième nombre facultatif pour l'opacité. Ce n'est l'argument de rien du
tout ; c'est un réglage dont le moteur se souvient. Tout ce qui est dessiné
après sort dans cette couleur jusqu'à ce que vous la changiez de nouveau,
exactement comme ça marche déjà pour `overlayBox` et `overlayLine`. Le schéma
est donc toujours le même : réglez la couleur, puis dessinez.
C'est la première chose que les gens font de travers avec les polices, et la
deuxième arrive dans quelques pages.
[#_quality]
=== La qualité
`fontQuality` décide comment les lettres sont rastérisées -- comment les courbes
d'une lettre sont transformées en pixels -- et il y a trois choix. Essayez
chacun en le mettant au-dessus de la ligne `fontLoad` et en regardant le
résultat de près.
`FONT_QUALITY_SOLID` est le plus rapide. Chaque pixel fait partie de la lettre
ou n'en fait pas partie, donc les diagonales et les courbes sortent avec des
marches d'escalier visibles. En 48 points, ça fait bon marché. En 12 points sur
un petit overlay, ça peut paraître *plus net* que les autres choix, et c'est
parfois ce que vous voulez.
`FONT_QUALITY_SHADED` lisse les bords, mais il le fait en les mélangeant avec la
couleur de fond actuelle, et il dessine ce fond comme un rectangle plein
derrière toute la chaîne. Vous obtenez une étiquette entourée d'une boîte, que
vous en vouliez une ou non. C'est la bonne réponse pour un afficheur de débogage
par-dessus une image chargée, et la mauvaise pour presque tout le reste.
`FONT_QUALITY_BLENDED` lisse les bords et les garde transparents, donc les
lettres se posent sur ce qu'il y a derrière elles. C'est le plus coûteux et
c'est ce que vous voulez presque tout le temps. Utilisez-le à moins d'avoir une
raison de ne pas le faire.
Le réglage est global, pas propre à chaque police, et il s'applique à partir du
moment où vous l'appelez. Les sprites que vous avez déjà faits gardent la
qualité avec laquelle ils ont été faits. L'entrée de la Référence consacrée à
`fontQuality` liste les trois noms et leurs valeurs.
[#_measuring_so_you_can_center_things]
=== Mesurer, pour pouvoir centrer
Tôt ou tard -- en général au bout d'une dizaine de minutes -- vous voulez le titre
au milieu de l'écran. Vous savez que l'écran fait `overlayGetWidth()` de large.
Vous ne savez pas quelle largeur fait le texte, et il n'y a pas d'appel
`fontMeasure` pour le demander.
Ce qu'il y a à la place, c'est `fontToSprite`. Il fait le rendu d'une chaîne
avec la police sélectionnée, la qualité actuelle et la couleur actuelle, et au
lieu de la mettre à l'écran, il vous renvoie un *sprite* : le même genre de
handle que celui que vous donne `spriteLoad`, ce qui veut dire que chaque appel
`sprite*` que vous avez appris à la leçon neuf marche dessus. Y compris les deux
qui le mesurent.
[source,lua]
----
fontSelect(titleFont)
colorForeground(255, 211, 90)
local titleImage = fontToSprite("ASTEROID PATROL")
----
Puis, dans `onOverlayUpdate` :
[source,lua]
----
local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2
spriteDraw(titleImage, x, 60)
----
C'est ça, centrer, et c'est tout le secret. Prenez la largeur de l'espace,
retirez la largeur de la chose, et la moitié de ce qui reste est l'écart de
chaque côté.
L'alignement à droite, c'est la même idée avec moins de calcul. Pour mettre du
texte contre la marge de droite :
[source,lua]
----
spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y)
----
`spriteGetHeight` mesure l'autre direction, pour empiler des lignes ou centrer
verticalement.
Il y a un piège dans le premier bloc de code, et c'est la deuxième chose que
tout le monde fait de travers. *La couleur est figée quand le sprite est
fabriqué, pas quand il est dessiné.* Un `colorForeground` au-dessus de
`fontToSprite` change le texte. Le mettre plutôt au-dessus de `spriteDraw` ne
fait rien du tout, parce que `colorForeground` est un réglage pour dessiner du
texte et des traits, et qu'un sprite est dessiné avec ses propres pixels. Si
votre titre soigneusement coloré sort en blanc, voilà pourquoi.
On peut recolorer un sprite après l'avoir fabriqué, mais avec un appel de
sprite, pas avec `colorForeground`. `spriteSetTint(image, red, green, blue)`
multiplie chaque pixel du sprite par cette couleur chaque fois qu'il est
dessiné, donc un texte rendu en blanc peut être dessiné en jaune aujourd'hui et
en rouge demain sans être refait. Le blanc est la couleur dans laquelle faire le
rendu quand vous comptez teinter : tout ce qui est multiplié par le blanc reste
ce qu'il était, donc la teinte est exactement la couleur que vous demandez.
[#_what_printing_costs]
=== Ce que coûte l'affichage
La première fois que `fontPrint` voit une chaîne, il fait un vrai travail : il
rastérise chaque lettre à partir des contours de la police et construit une
image de toute la chaîne. Puis il garde cette image. La fois suivante où vous
affichez la même chaîne, avec la même police, la même qualité et les mêmes
couleurs, il saute tout ça et copie l'image qu'il a déjà, ce qui coûte à peu
près autant que dessiner un sprite. Une chaîne que vous arrêtez d'afficher est
lâchée une seconde plus tard, donc rien ne s'accumule.
Ça veut dire que la façon évidente d'écrire un écran titre est aussi la plus
rapide. Afficher `"ASTEROID PATROL"` soixante fois par seconde n'en fait le
rendu qu'une fois. Un score qui change deux fois par seconde est rendu deux fois
par seconde, tout seul, sans aucune comptabilité de votre part.
Le seul cas qui coûte, c'est du texte qui est *différent* à chaque image -- un
minuteur en millisecondes, un compteur d'images -- parce que chaque image est une
chaîne que `fontPrint` n'a jamais vue. Sur un ordinateur de bureau, ça n'a pas
d'importance. Sur un Raspberry Pi ou une console portable, un plein écran de ce
genre de texte en aura. Le remède est de changer le texte moins souvent : un
minuteur en secondes entières est une nouvelle chaîne une fois par seconde, pas
soixante.
Alors pourquoi cette leçon a-t-elle utilisé `fontToSprite` ? Parce qu'un sprite
peut faire des choses que le texte affiché ne peut pas faire :
* *On peut le mesurer*, et c'est la seule façon de centrer du texte ou de
l'aligner contre le côté droit.
* *On peut le redimensionner et le faire tourner* avec les appels `sprite*` de
la leçon neuf.
* *Il peut survivre à sa police*, ce qui va compter dans un instant.
Utilisez `fontPrint` pour le texte, et `fontToSprite` quand vous avez besoin de
l'une de ces choses.
Le score de cette leçon a besoin de l'une d'elles : il est placé contre la marge
de droite, donc il doit être mesuré, donc il doit être un sprite. Et un sprite
de score n'est juste que jusqu'à ce que le score change. Quand il change, faites
le rendu d'un nouveau sprite et déchargez l'ancien -- l'oublier laisse fuir un
sprite à chaque changement :
[source,lua]
----
local function renderScore()
if scoreImage ~= nil then
spriteUnload(scoreImage)
end
fontSelect(scoreFont)
colorForeground(255, 255, 255)
scoreImage = fontToSprite(string.format("%06d", score))
scoreShown = score
end
----
`scoreShown` retient quel score ce sprite affiche, pour que le code de dessin
puisse savoir s'il est encore juste :
[source,lua]
----
if score ~= scoreShown then
renderScore()
end
----
`string.format("%06d", score)` est du Lua tout simple, pas du Singe. Il
transforme un nombre en chaîne, et `%06d` veut dire « en nombre entier, au moins
six chiffres, complété par des zéros », c'est pourquoi les scores d'arcade ont
des zéros devant. Changez-le en `%d` et les zéros disparaissent.
[#_putting_fonts_away]
=== Ranger les polices
`fontUnload` ferme une police et rend son handle invalide. Les sprites que vous
avez faits avec `fontToSprite` ne sont pas touchés -- une fois rendu, un sprite
n'a plus rien à voir avec la police -- donc vous pouvez décharger une police et
continuer de dessiner le texte fait avec elle.
Un point délicat : si vous déchargez la police qui était sélectionnée, *aucune*
police n'est sélectionnée ensuite, et le `fontPrint` suivant termine votre jeu
avec une erreur. Sélectionnez-en d'abord une autre si vous comptez continuer
d'afficher du texte.
L'endroit bien rangé pour tout ça, c'est `onShutdown`, un callback que le moteur
appelle une fois quand le jeu se ferme :
[source,lua]
----
function onShutdown()
spriteUnload(titleImage)
fontUnload(titleFont)
end
----
Singe fait le ménage derrière vous quand le jeu se termine, donc rien ne prend
feu si vous sautez cette étape. Écrivez-la quand même. Un jeu qui libère ce
qu'il a chargé est un jeu qu'on peut charger deux fois.
[#_the_whole_script]
=== Le script complet
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
local MARGIN = 24
local TITLE_Y = 60
local SCORE_Y = 170
local LABEL_Y = 186
local HINT_Y = 300
local CLOCK_Y = 430
local score = 0
local titleFont = nil
local scoreFont = nil
local smallFont = nil
local titleImage = nil
local hintImage = nil
local scoreImage = nil
local scoreShown = nil
local function centered(image)
return (overlayGetWidth() - spriteGetWidth(image)) / 2
end
local function renderScore()
if scoreImage ~= nil then
spriteUnload(scoreImage)
end
fontSelect(scoreFont)
colorForeground(255, 255, 255)
scoreImage = fontToSprite(string.format("%06d", score))
scoreShown = score
end
fontQuality(FONT_QUALITY_BLENDED)
titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36)
smallFont = fontLoad("Singe/FreeSansBold.ttf", 18)
fontSelect(titleFont)
colorForeground(255, 211, 90)
titleImage = fontToSprite("ASTEROID PATROL")
fontSelect(smallFont)
colorForeground(140, 200, 255)
hintImage = fontToSprite("Press the fire button to score")
renderScore()
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
score = score + 125
end
end
function onOverlayUpdate()
overlayClear()
spriteDraw(titleImage, centered(titleImage), TITLE_Y)
if score ~= scoreShown then
renderScore()
end
spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), SCORE_Y)
fontSelect(smallFont)
colorForeground(160, 160, 160)
fontPrint(MARGIN, LABEL_Y, "SCORE")
fontPrint(MARGIN, CLOCK_Y, "Running for " .. (singeGetTicks() // 1000) .. " seconds")
if (singeGetTicks() // 500) % 2 == 0 then
spriteDraw(hintImage, centered(hintImage), HINT_Y)
end
return OVERLAY_UPDATED
end
function onShutdown()
spriteUnload(titleImage)
spriteUnload(hintImage)
if scoreImage ~= nil then
spriteUnload(scoreImage)
end
fontUnload(titleFont)
fontUnload(scoreFont)
fontUnload(smallFont)
end
----
Lancez-le et maintenez le bouton de tir. Le titre et l'indication sont centrés,
le score grimpe contre la marge de droite et n'est rendu de nouveau que lorsqu'il
change, l'indication clignote, et l'étiquette et l'horloge sont affichées avec
`fontPrint` à chaque image -- l'horloge en secondes entières, donc ce n'est une
nouvelle chaîne qu'une fois par seconde.
[#_what_just_happened_15]
=== Ce qui vient de se passer
[source,lua]
----
titleFont = fontLoad("Singe/FreeSansBold.ttf", 48)
scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36)
smallFont = fontLoad("Singe/FreeSansBold.ttf", 18)
----
Un fichier, trois tailles, trois handles. Chacun coûte de la mémoire pour son
propre jeu de glyphes rendus, donc chargez les tailles dont vous vous servez et
pas plus. Trois, c'est normal. Une douzaine, ça sent mauvais.
Après ces trois lignes, la police sélectionnée est `smallFont`, parce que
charger sélectionne. C'est pourquoi le code appelle `fontSelect(titleFont)` avant
de faire le rendu du titre : la dernière chose chargée n'est pas celle que vous
voulez en premier.
[source,lua]
----
local function centered(image)
return (overlayGetWidth() - spriteGetWidth(image)) / 2
end
----
Écrite une fois comme fonction parce qu'elle sert deux fois, et parce que la
prochaine fois que vous voudrez centrer quelque chose, vous en aurez de nouveau
besoin. Donnez un nom au calcul et vous n'aurez plus jamais à le relire.
[source,lua]
----
if (singeGetTicks() // 500) % 2 == 0 then
spriteDraw(hintImage, centered(hintImage), HINT_Y)
end
----
`singeGetTicks()` est le nombre de millisecondes écoulées depuis le démarrage du
moteur. `//` est la division qui jette le reste, donc `singeGetTicks() // 500`
compte des demi-secondes, et `% 2` est le reste de la division par deux, qui
vaut `0`, `1`, `0`, `1`... Donc l'indication est dessinée pendant une
demi-seconde et sautée pendant une demi-seconde. Ne rien dessiner, c'est comme
ça qu'on fait clignoter quelque chose.
[source,lua]
----
fontSelect(smallFont)
colorForeground(160, 160, 160)
fontPrint(MARGIN, LABEL_Y, "SCORE")
----
Les deux réglages sont nécessaires ici, et ça vaut la peine de dire clairement
pourquoi. Dessiner un sprite ne change ni la police sélectionnée ni la couleur,
donc les deux appels `spriteDraw` au-dessus les laissent exactement comme ils
les ont trouvées. Mais le code de mise en place a sélectionné trois polices
différentes à tour de rôle, et `renderScore` en sélectionne une autre chaque
fois que le score bouge, donc la sélection au moment où cette ligne s'exécute
est celle qui s'est trouvée choisie en dernier. N'essayez pas de la suivre.
Sélectionnez la police et réglez la couleur juste avant d'afficher, à chaque
fois. Ça fait deux lignes, et ça supprime toute une catégorie de soirées
perplexes.
[#_try_it_19]
=== Essayez
. *Changez la qualité.* Mettez `FONT_QUALITY_SOLID` dans la ligne
`fontQuality` et regardez la courbe du `S` dans `ASTEROID`. Puis essayez
`FONT_QUALITY_SHADED` et regardez ce qui se passe derrière chaque chaîne.
. *Mettez l'indication en jaune.* Déplacez la ligne
`colorForeground(140, 200, 255)` d'au-dessus de `fontToSprite` à juste
au-dessus du `spriteDraw` qui dessine `hintImage`, et changez les nombres.
Trouvez pourquoi rien ne se passe. Puis remettez-la, faites plutôt
l'indication en blanc, et rendez-la jaune avec `spriteSetTint` juste avant
qu'elle soit dessinée.
. *Alignez l'horloge à droite.* L'horloge est dessinée avec `fontPrint`, qu'on
ne peut pas mesurer. Mettez-la quand même contre la marge de droite. Il n'y a
qu'une seule façon de le faire, et la trouver, c'est tout l'intérêt.
. *Retirez la vérification de `scoreShown`*, pour que `renderScore()`
s'exécute plutôt à chaque image. Ça aura exactement la même allure, mais ça
fait maintenant le rendu du score soixante fois par seconde au lieu de le
faire quand il change -- exactement le travail que `fontPrint` vous épargne
quand vous le laissez faire. Puis retirez aussi le `spriteUnload` de
`renderScore`, laissez tourner une minute, et surveillez la mémoire
qu'utilise votre jeu.
. *Affichez une horloge en millisecondes.* Changez la ligne de l'horloge pour
qu'elle affiche `singeGetTicks()` sans le `// 1000`. Ça marche toujours ;
c'est maintenant une chaîne que `fontPrint` n'a jamais vue, à chaque image.
Sur une machine lente, c'est ce genre de texte qui coûte.
. *Utilisez votre propre police.* Trouvez un `.ttf` sur votre machine,
copiez-le à côté de votre script, et chargez-le avec `DIR .. "thatfont.ttf"`.
Certaines polices sont affreuses en 18 points et très bien en 48. C'est la
faute de la police, pas la vôtre.
[#_break_it_on_purpose_18]
=== Cassez-le exprès
Ajoutez une ligne `fontPrint(24, 60, "SCORE")` juste sous
`overlaySetResolution(discGetWidth(), discGetHeight())`, pour qu'elle s'exécute
avant tout `fontLoad`. Singe démarre et meurt aussitôt :
----
4:fontPrint: No font selected.
----
Lisez-le comme n'importe quelle autre erreur. `4` est la ligne. `fontPrint` est
l'appel du moteur qui s'est plaint -- un message de cette forme vient de
l'intérieur du moteur plutôt que de Lua, donc il nomme la fonction au lieu du
fichier. Le reste, c'est la plainte, et ici elle est exacte : vous lui avez
demandé d'afficher du texte et il n'y avait pas de police pour le faire.
Vous rencontrerez celle-ci pour de vrai, d'une façon moins évidente. Déchargez
la police sélectionnée pendant que le jeu tourne, et le `fontPrint` suivant dit
exactement la même chose, à plusieurs centaines de lignes du `fontUnload` qui
l'a causée.
[#_what_you_learned_19]
=== Ce que vous avez appris
* `fontLoad` ouvre un fichier TrueType à une taille en points fixe et renvoie
un handle. Une taille, un handle.
* Charger une police la sélectionne ; `fontSelect` choisit entre les polices
chargées.
* `fontPrint` compte en pixels et `overlayPrint` compte en cellules de
caractère.
* La couleur n'est pas un argument. `colorForeground` est un réglage ;
réglez-le, puis dessinez.
* `fontQuality` choisit comment les lettres sont rastérisées, et
`FONT_QUALITY_BLENDED` est la réponse habituelle.
* `fontPrint` garde ce dont il a fait le rendu, donc afficher le même texte à
chaque image coûte peu ; c'est le texte différent à chaque image qui coûte.
* `fontToSprite` fait le rendu dans un sprite, que `spriteGetWidth` mesure et
que les appels `sprite*` peuvent redimensionner et faire tourner. Utilisez-le
pour ça, pas pour la vitesse.
* Mesurer est la façon de centrer ou d'aligner du texte à droite, parce que rien
d'autre ne peut vous dire quelle largeur fera une chaîne.
* `colorForeground` atteint le texte quand il est fabriqué, jamais un sprite
quand il est dessiné ; `spriteSetTint` est la façon de recolorer un sprite
après coup.
* `fontUnload` libère une police ; les sprites qu'elle a servi à faire lui
survivent.
[#_next_time_19]
=== La prochaine fois
Du texte qui a de l'allure, c'est encore loin d'un menu qui marche. Une page
avec un titre, une liste dans laquelle on déplace un curseur, une glissière, et
un bouton, c'est une centaine de lignes de dessin et de calculs de curseur si
vous la construisez avec les appels de cette leçon. La leçon vingt ne la
construit pas du tout avec ces appels.