[#_lesson_19_text_that_looks_good] == Lektion 19: Text, der gut aussieht image::learn/19-text.png[Die fertige Lektion, 480] Jedes Wort, das du seit Lektion eins auf den Bildschirm gebracht hast, kam aus `overlayPrint`, und `overlayPrint` hat genau eine Schriftart: einen Pixelblock fester Breite, der in die Engine eingebaut ist, für immer gleich groß, in welcher Farbe er gerade eben ist. Für einen Frame-Zähler ist sie perfekt. Für einen Titel, eine Punktzahl, ein Menü oder alles, was ein Spieler ansehen und nicht einfach überlesen soll, ist sie falsch. Diese Lektion lädt eine echte Schriftart von der Festplatte, gibt Text mit ihr aus, färbt ihn ein, misst ihn aus, damit du Dinge zentrieren kannst, und zeigt dir, was das Ausgeben von Text wirklich kostet. Am Ende hast du einen Titelbildschirm, der gewollt aussieht statt zufällig. [#_set_the_overlay_to_a_size_you_can_think_in] === Gib dem Overlay eine Größe, in der du denken kannst Fang eine neue Datei namens `text.singe` mit zwei Zeilen an: [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) ---- Die erste Zeile hast du schon oft geschrieben; sie lädt das Framework, das `DIR`, die Namen `SWITCH_*` und die Namen `FONT_QUALITY_*` definiert, die du gleich benutzt. Die zweite ist wichtiger, als sie aussieht. Das Overlay -- die Fläche, auf der alles landet, was du zeichnest -- ist am Anfang *halb* so groß wie die Welt. Ein Spiel ohne Video hat eine Zeichenfläche von 720 mal 480, also ist das Overlay am Anfang 360 mal 240 groß, und eine Schrift in 36 Punkt ist auf einem Overlay, das 360 Pixel breit ist, riesig. `overlaySetResolution` ersetzt das Overlay durch eines in der Größe, um die du bittest, und von da an liegt jede Koordinate, die du angibst, und alles, was `overlayGetWidth` und `overlayGetHeight` dir sagen, in diesem Raum. `discGetWidth()` und `discGetHeight()` antworten mit der Größe des Videos, wenn es eines gibt, und mit der Größe der Zeichenfläche, wenn nicht, also bekommst du mit dieser einen Zeile ein Overlay, das zur Welt passt, egal welche Art von Spiel das hier ist. Ruf es einmal auf, am Anfang, bevor du irgendetwas zeichnest. [#_load_a_font] === Lade eine Schriftart Schreib darunter: [source,lua] ---- fontQuality(FONT_QUALITY_BLENDED) local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) ---- `fontLoad` öffnet eine TrueType-Schriftdatei und gibt dir eine Zahl zurück -- ein *Handle* -- und die benutzt du immer, wenn du diese Schriftart willst. Das zweite Argument ist die Punktgröße, und die steht fest, solange es das Handle gibt. Es gibt keine Möglichkeit, die Größe einer geladenen Schriftart nachträglich zu ändern. Wenn du dieselbe Schrift in drei Größen willst, rufst du `fontLoad` dreimal auf und behältst drei Handles. `Singe/FreeSansBold.ttf` ist die Schriftart, die die Engine in den Ordner `Singe` in deinem Arbeitsordner entpackt hat, als du zum ersten Mal irgendetwas gestartet hast. Jede Kopie von Singe hat sie, also kannst du dich auf sie verlassen. Jede andere `.ttf`-Datei funktioniert genauso; leg sie neben dein Skript und lade sie als `DIR .. "myfont.ttf"`. Gib jetzt Text mit ihr aus: [source,lua] ---- function onOverlayUpdate() overlayClear() fontPrint(24, 60, "ASTEROID PATROL") return OVERLAY_UPDATED end ---- Starte es. Große weiße Buchstaben oben links. Achte darauf, was du *nicht* getan hast: Du hast `fontPrint` nie gesagt, welche Schriftart es benutzen soll. `fontLoad` macht die Schriftart, die es gerade geladen hat, zur ausgewählten, und `fontPrint` zeichnet immer mit der, die gerade ausgewählt ist. Wenn mehr als eine Schriftart geladen ist, wählst du mit `fontSelect` zwischen ihnen, und das machst du gleich. Achte auch darauf, dass `24` und `60` Pixel sind, keine Zeichenzellen. Das ist der Unterschied zwischen `fontPrint` und `overlayPrint`, über den die Leute stolpern: `overlayPrint(2, 2, ...)` heißt zwei Buchstaben nach rechts und zwei Zeilen nach unten, `fontPrint(2, 2, ...)` dagegen zwei Pixel nach rechts und zwei Pixel nach unten, also eingequetscht in die Ecke. Das `y`, das du angibst, ist die *Oberkante* des Texts, nicht die Grundlinie, auf der er steht. [#_color] === Farbe `fontPrint` nimmt drei Argumente, und keines davon ist eine Farbe. Die Farbe kommt von woanders: [source,lua] ---- colorForeground(255, 211, 90) fontPrint(24, 60, "ASTEROID PATROL") ---- `colorForeground` setzt Rot, Grün und Blau von `0` bis `255` und, wenn du willst, eine vierte Zahl für die Deckkraft. Es ist kein Argument für irgendetwas; es ist eine Einstellung, die sich die Engine merkt. Alles, was danach gezeichnet wird, kommt in dieser Farbe heraus, bis du sie wieder änderst, genau so, wie `overlayBox` und `overlayLine` schon funktionieren. Das Muster ist also immer dasselbe: Setz die Farbe, dann zeichne. Das ist das Erste, was die Leute bei Schriftarten falsch machen, und das Zweite kommt in ein paar Seiten. [#_quality] === Qualität `fontQuality` entscheidet, wie die Buchstaben gerastert werden, also wie die Kurven eines Buchstabens zu Pixeln werden. Es gibt drei Möglichkeiten. Probier jede aus, indem du sie über die Zeile mit `fontLoad` setzt und dir das Ergebnis genau ansiehst. `FONT_QUALITY_SOLID` ist die schnellste. Jedes Pixel gehört entweder zum Buchstaben oder nicht, also kommen die Diagonalen und Kurven mit sichtbaren Treppenstufen heraus. Bei 48 Punkt sieht das billig aus. Bei 12 Punkt auf einem kleinen Overlay kann es *schärfer* aussehen als die anderen, und manchmal willst du genau das. `FONT_QUALITY_SHADED` glättet die Kanten, aber dazu mischt es sie mit der aktuellen Hintergrundfarbe, und diesen Hintergrund zeichnet es als volles Rechteck hinter den ganzen String. Du bekommst ein Etikett mit einem Kasten drumherum, ob du einen wolltest oder nicht. Für ein Debug-Anzeigefeld über einem unruhigen Bild ist das die richtige Antwort und für fast alles andere die falsche. `FONT_QUALITY_BLENDED` glättet die Kanten und lässt sie durchsichtig, also sitzen die Buchstaben auf dem, was gerade hinter ihnen ist. Sie kostet am meisten, und sie ist das, was du fast immer willst. Nimm sie, außer du hast einen Grund dagegen. Die Einstellung ist global, nicht pro Schriftart, und sie gilt ab dem Moment, in dem du sie aufrufst. Sprites, die du schon gemacht hast, behalten die Qualität, mit der sie gemacht wurden. Der Eintrag zu `fontQuality` in der Referenz nennt die drei Namen und ihre Werte. [#_measuring_so_you_can_center_things] === Ausmessen, damit du Dinge zentrieren kannst Früher oder später -- meistens nach etwa zehn Minuten -- willst du den Titel in der Mitte des Bildschirms haben. Du weißt, dass der Bildschirm `overlayGetWidth()` breit ist. Du weißt nicht, wie breit der Text ist, und es gibt keinen Aufruf `fontMeasure`, den du fragen könntest. Was es stattdessen gibt, ist `fontToSprite`. Es rendert einen String mit der ausgewählten Schriftart, der aktuellen Qualität und der aktuellen Farbe, und statt ihn auf den Bildschirm zu bringen, gibt es dir ein *Sprite* zurück: dieselbe Art von Handle, die dir `spriteLoad` gibt, und das heißt, jeder `sprite*`-Aufruf, den du in Lektion neun gelernt hast, funktioniert damit. Auch die beiden, die es ausmessen. [source,lua] ---- fontSelect(titleFont) colorForeground(255, 211, 90) local titleImage = fontToSprite("ASTEROID PATROL") ---- Und dann, in `onOverlayUpdate`: [source,lua] ---- local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2 spriteDraw(titleImage, x, 60) ---- Das ist Zentrieren, und das ist schon der ganze Trick. Nimm die Breite des Platzes, zieh die Breite des Dings ab, und die Hälfte von dem, was übrig bleibt, ist der Abstand auf jeder Seite. Rechtsbündig ist dieselbe Idee mit weniger Rechnerei. So setzt du Text an den rechten Rand: [source,lua] ---- spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y) ---- `spriteGetHeight` misst die andere Richtung, um Zeilen zu stapeln oder senkrecht zu zentrieren. In diesem ersten Codeblock steckt ein Haken, und das ist das Zweite, was alle falsch machen. *Die Farbe wird eingebacken, wenn das Sprite gemacht wird, nicht wenn es gezeichnet wird.* `colorForeground` über `fontToSprite` ändert den Text. Setzt du es stattdessen über `spriteDraw`, tut es überhaupt nichts, denn `colorForeground` ist eine Einstellung für das Zeichnen von Text und Linien, und ein Sprite wird mit seinen eigenen Pixeln gezeichnet. Wenn dein sorgfältig eingefärbter Titel weiß herauskommt, liegt es daran. Ein Sprite lässt sich umfärben, nachdem es gemacht wurde, aber mit einem Sprite-Aufruf, nicht mit `colorForeground`. `spriteSetTint(image, red, green, blue)` multipliziert jedes Pixel des Sprites bei jedem Zeichnen mit dieser Farbe, also kann Text, der in Weiß gerendert wurde, heute gelb und morgen rot gezeichnet werden, ohne neu gemacht zu werden. Weiß ist die Farbe, in der du renderst, wenn du einfärben willst: Alles, was mit Weiß multipliziert wird, bleibt, was es war, also ist die Tönung genau die Farbe, um die du bittest. [#_what_printing_costs] === Was Ausgeben kostet Wenn `fontPrint` einen String zum ersten Mal sieht, leistet es echte Arbeit: Es rastert jeden Buchstaben aus den Umrissen der Schriftart und baut ein Bild des ganzen Strings. Dann behält es dieses Bild. Wenn du denselben String das nächste Mal ausgibst, mit derselben Schriftart, Qualität und denselben Farben, überspringt es all das und kopiert das Bild, das es schon hat, und das kostet ungefähr so viel wie das Zeichnen eines Sprites. Ein String, den du nicht mehr ausgibst, wird eine Sekunde später losgelassen, also häuft sich nichts an. Das heißt, die naheliegende Art, einen Titelbildschirm zu schreiben, ist auch die schnelle. `"ASTEROID PATROL"` sechzigmal pro Sekunde auszugeben rendert ihn einmal. Eine Punktzahl, die sich zweimal pro Sekunde ändert, wird zweimal pro Sekunde gerendert, von selbst, ohne dass du Buch führen musst. Der eine Fall, der etwas kostet, ist Text, der in jedem Frame *anders* ist (ein Timer in Millisekunden, ein Frame-Zähler), denn jeder Frame ist ein String, den `fontPrint` noch nie gesehen hat. Auf einem Desktop-Rechner spielt das keine Rolle. Auf einem Raspberry Pi oder einem Handheld spielt ein Bildschirm voll davon sehr wohl eine. Die Abhilfe ist, den Text seltener zu ändern: Ein Timer in ganzen Sekunden ist einmal pro Sekunde ein neuer String, nicht sechzigmal. Warum hat diese Lektion dann überhaupt `fontToSprite` benutzt? Weil ein Sprite Dinge kann, die ausgegebener Text nicht kann: * *Es kann ausgemessen werden*, und das ist die einzige Möglichkeit, Text zu zentrieren oder an der rechten Seite auszurichten. * *Es kann skaliert und gedreht werden*, mit den `sprite*`-Aufrufen aus Lektion neun. * *Es kann seine Schriftart überleben*, und das ist gleich wichtig. Nimm `fontPrint` für Text und `fontToSprite`, wenn du eines davon brauchst. Die Punktzahl in dieser Lektion braucht eines davon: Sie sitzt am rechten Rand, also muss sie ausgemessen werden, also muss sie ein Sprite sein. Und ein Sprite einer Punktzahl stimmt nur, bis sich die Punktzahl ändert. Wenn sie das tut, render ein neues Sprite und entlade das alte -- vergisst du das, bleibt bei jeder Änderung ein Sprite als Leck zurück: [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` merkt sich, welche Punktzahl dieses Sprite zeigt, damit der Zeichencode erkennen kann, ob es noch stimmt: [source,lua] ---- if score ~= scoreShown then renderScore() end ---- `string.format("%06d", score)` ist reines Lua, nicht Singe. Es macht aus einer Zahl einen String, und `%06d` heißt „als ganze Zahl, mindestens sechs Stellen, mit Nullen aufgefüllt“, und darum haben Arcade-Punktzahlen führende Nullen. Ändere es in `%d`, und die Nullen verschwinden. [#_putting_fonts_away] === Schriftarten wegräumen `fontUnload` schließt eine Schriftart und macht ihr Handle ungültig. Sprites, die du mit `fontToSprite` gemacht hast, sind davon nicht betroffen (ist ein Sprite einmal gerendert, hat es mit der Schriftart nichts mehr zu tun), also kannst du eine Schriftart entladen und weiter Text zeichnen, der aus ihr gemacht wurde. Eine scharfe Kante: Wenn du die ausgewählte Schriftart entlädst, ist danach *keine* Schriftart ausgewählt, und das nächste `fontPrint` beendet dein Spiel mit einem Fehler. Wähl zuerst eine andere aus, wenn du weiter Text ausgeben willst. Der ordentliche Platz für all das ist `onShutdown`, ein Callback, den die Engine einmal aufruft, wenn das Spiel beendet wird: [source,lua] ---- function onShutdown() spriteUnload(titleImage) fontUnload(titleFont) end ---- Singe räumt hinter dir auf, wenn das Spiel endet, also fängt nichts Feuer, wenn du das weglässt. Schreib es trotzdem. Ein Spiel, das freigibt, was es geladen hat, ist ein Spiel, das du zweimal laden kannst. [#_the_whole_script] === Das ganze Skript [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 ---- Starte es und halt den Feuerknopf gedrückt. Der Titel und der Hinweis sitzen zentriert, die Punktzahl klettert am rechten Rand hoch und wird nur neu gerendert, wenn sie sich ändert, der Hinweis blinkt, und das Etikett und die Uhr werden in jedem Frame mit `fontPrint` ausgegeben -- die Uhr in ganzen Sekunden, also ist sie nur einmal pro Sekunde ein neuer String. [#_what_just_happened_15] === Was gerade passiert ist [source,lua] ---- titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36) smallFont = fontLoad("Singe/FreeSansBold.ttf", 18) ---- Eine Datei, drei Größen, drei Handles. Jedes kostet Speicher für seinen eigenen Satz gerenderter Glyphen, also lade die Größen, die du benutzt, und keine weiteren. Drei sind normal. Ein Dutzend riecht verdächtig. Nach diesen drei Zeilen ist `smallFont` die ausgewählte Schriftart, denn Laden wählt aus. Darum ruft der Code `fontSelect(titleFont)` auf, bevor er den Titel rendert: Das zuletzt Geladene ist nicht das, was du zuerst willst. [source,lua] ---- local function centered(image) return (overlayGetWidth() - spriteGetWidth(image)) / 2 end ---- Einmal als Funktion geschrieben, weil es zweimal benutzt wird und weil du es beim nächsten Mal, wenn du etwas zentriert haben willst, wieder brauchst. Gib der Rechnung einen Namen, und du musst sie nie wieder lesen. [source,lua] ---- if (singeGetTicks() // 500) % 2 == 0 then spriteDraw(hintImage, centered(hintImage), HINT_Y) end ---- `singeGetTicks()` ist die Zahl der Millisekunden seit dem Start der Engine. `//` ist eine Division, die den Rest wegwirft, also zählt `singeGetTicks() // 500` halbe Sekunden, und `% 2` ist der Rest nach dem Teilen durch zwei, also `0`, `1`, `0`, `1` ... Der Hinweis wird also eine halbe Sekunde lang gezeichnet und eine halbe Sekunde lang ausgelassen. Nichts zu zeichnen ist die Art, wie man etwas blinken lässt. [source,lua] ---- fontSelect(smallFont) colorForeground(160, 160, 160) fontPrint(MARGIN, LABEL_Y, "SCORE") ---- Hier werden beide Einstellungen gebraucht, und es lohnt sich, klar zu sehen, warum. Ein Sprite zu zeichnen ändert weder die ausgewählte Schriftart noch die Farbe, also lassen die beiden `spriteDraw`-Aufrufe darüber beides genau so, wie sie es vorgefunden haben. Aber der Code am Anfang hat nacheinander drei verschiedene Schriftarten ausgewählt, und `renderScore` wählt jedes Mal eine weitere aus, wenn sich die Punktzahl bewegt, also ist die Auswahl, wenn diese Zeile läuft, die, die zufällig zuletzt gewählt wurde. Versuch nicht, darüber Buch zu führen. Wähl die Schriftart aus und setz die Farbe unmittelbar bevor du etwas ausgibst, jedes Mal. Das sind zwei Zeilen, und sie ersparen dir eine ganze Kategorie ratloser Abende. [#_try_it_19] === Probier es aus . *Ändere die Qualität.* Schreib `FONT_QUALITY_SOLID` in die Zeile mit `fontQuality` und sieh dir die Kurve des `S` in `ASTEROID` an. Dann probier `FONT_QUALITY_SHADED` und achte darauf, was hinter jedem String passiert. . *Mach den Hinweis gelb.* Verschieb die Zeile `colorForeground(140, 200, 255)` von über `fontToSprite` direkt über das `spriteDraw`, das `hintImage` zeichnet, und ändere die Zahlen. Finde heraus, warum nichts passiert. Dann setz sie zurück, mach den Hinweis stattdessen weiß und färb ihn mit `spriteSetTint` gelb, direkt bevor er gezeichnet wird. . *Richte die Uhr rechtsbündig aus.* Die Uhr wird mit `fontPrint` gezeichnet, und das lässt sich nicht ausmessen. Bring sie trotzdem an den rechten Rand. Es gibt nur einen Weg, und ihn zu finden ist der Sinn der Sache. . *Nimm die Prüfung von `scoreShown` heraus*, sodass `renderScore()` stattdessen in jedem Frame läuft. Es sieht genauso aus, aber jetzt wird die Punktzahl sechzigmal pro Sekunde gerendert statt dann, wenn sie sich ändert -- genau die Arbeit, die `fontPrint` dir erspart, wenn du es lässt. Dann nimm auch das `spriteUnload` in `renderScore` heraus, lass es eine Minute laufen und beobachte, wie viel Speicher dein Spiel benutzt. . *Gib eine Uhr in Millisekunden aus.* Ändere die Zeile der Uhr so, dass sie `singeGetTicks()` ohne das `// 1000` ausgibt. Es funktioniert immer noch; es ist jetzt in jedem Frame ein String, den `fontPrint` noch nie gesehen hat. Auf einem langsamen Rechner ist das die Art von Text, die etwas kostet. . *Nimm deine eigene Schriftart.* Such dir eine `.ttf` auf deinem Rechner, kopier sie neben dein Skript und lade sie mit `DIR .. "thatfont.ttf"`. Manche Schriftarten sehen bei 18 Punkt furchtbar aus und bei 48 gut. Das ist die Schuld der Schriftart, nicht deine. [#_break_it_on_purpose_18] === Mach es absichtlich kaputt Füg eine Zeile `fontPrint(24, 60, "SCORE")` direkt unter `overlaySetResolution(discGetWidth(), discGetHeight())` ein, sodass sie läuft, bevor irgendein `fontLoad` gelaufen ist. Singe startet und stirbt sofort: ---- 4:fontPrint: No font selected. ---- Lies das wie jeden anderen Fehler. `4` ist die Zeile. `fontPrint` ist der Engine-Aufruf, der sich beschwert hat -- Meldungen in dieser Form kommen aus dem Inneren der Engine und nicht von Lua, darum nennen sie die Funktion statt der Datei. Der Rest ist die Beschwerde, und hier ist sie genau: Du hast verlangt, etwas auszugeben, und es gab keine Schriftart, mit der das ging. Diesem Fehler wirst du in echt auf eine weniger offensichtliche Art begegnen. Entlade die ausgewählte Schriftart, während das Spiel läuft, und das nächste `fontPrint` sagt genau dasselbe, mehrere hundert Zeilen entfernt von dem `fontUnload`, das es verursacht hat. [#_what_you_learned_19] === Was du gelernt hast * `fontLoad` öffnet eine TrueType-Datei in einer festen Punktgröße und gibt ein Handle zurück. Eine Größe, ein Handle. * Eine Schriftart zu laden wählt sie aus; `fontSelect` wählt zwischen geladenen Schriftarten. * `fontPrint` zählt in Pixeln und `overlayPrint` in Zeichenzellen. * Farbe ist kein Argument. `colorForeground` ist eine Einstellung; setz sie, dann zeichne. * `fontQuality` bestimmt, wie Buchstaben gerastert werden, und `FONT_QUALITY_BLENDED` ist die übliche Antwort. * `fontPrint` behält, was es rendert, also ist es billig, in jedem Frame denselben Text auszugeben; Text, der in jedem Frame anders ist, ist das, was kostet. * `fontToSprite` rendert in ein Sprite, das `spriteGetWidth` ausmisst und die `sprite*`-Aufrufe skalieren und drehen können. Nimm es dafür, nicht für Geschwindigkeit. * Ausmessen ist die Art, wie du Text zentrierst oder rechtsbündig ausrichtest, denn sonst kann dir nichts sagen, wie breit ein String wird. * `colorForeground` erreicht Text, wenn er gemacht wird, nie ein Sprite, wenn es gezeichnet wird; mit `spriteSetTint` färbst du ein Sprite nachträglich um. * `fontUnload` gibt eine Schriftart frei; die Sprites, die sie gemacht hat, überleben sie. [#_next_time_19] === Beim nächsten Mal Text, der gut aussieht, ist noch weit entfernt von einem Menü, das funktioniert. Eine Seite mit einem Titel, einer Liste, durch die du einen Cursor bewegst, einem Schieberegler und einem Knopf sind hundert Zeilen Zeichnen und Cursor-Rechnerei, wenn du sie aus den Aufrufen dieser Lektion baust. Lektion zwanzig baut sie überhaupt nicht aus diesen Aufrufen.