[#_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.