[#_lesson_21_subtitles] == Leçon 21 : Les sous-titres image::learn/21-subtitles.png[La leçon terminée, 480] Quelqu'un qui n'entend pas va jouer à votre jeu. Si votre jeu a des paroles et pas de sous-titres, cette personne ne le pourra pas. Pas « ce sera plus difficile pour elle » -- elle arrivera à la première scène où un personnage dit ce qui explique au joueur quoi faire, et elle s'arrêtera là. C'est tout, et c'est la raison pour laquelle cette leçon existe au lieu d'être un paragraphe de la leçon dix-neuf. L'autre raison est pratique. Ajouter des sous-titres après coup à un jeu terminé est une misère. Il faut retrouver chaque réplique, déterminer sur quelle image elle tombe, et découvrir que la moitié de vos synchronisations sont fausses parce que votre vidéo a été réencodée au milieu du projet. Fait pendant que vous écrivez le jeu, c'est un après-midi. Singe vous donne un système qui fait la partie difficile, et cette leçon explique comment vous en servir. [#_it_is_not_ordinary_text_drawing] === Ce n'est pas du texte ordinaire Vous pourriez faire les sous-titres vous-même. Garder une liste de lignes avec leurs heures de début et de fin, regarder l'horloge dans `onOverlayUpdate`, et dessiner la ligne du moment avec `fontToSprite`. Certains l'ont fait. Voici pourquoi vous ne devriez pas. Les sous-titres sont synchronisés sur *le disque*, pas sur une horloge. Quand `srtLoad` lit votre fichier, elle convertit chaque horodatage en numéro d'image du disque, sur-le-champ, en utilisant la fréquence d'images du disque chargé. À partir de là, le moteur ne regarde plus du tout une horloge ; à chaque image dessinée, il demande quel sous-titre couvre l'image sur laquelle se trouve le disque. Cette seule décision vous offre ce qu'un système fait maison ne réussit jamais : *un sous-titre survit à un saut*. Envoyez le disque n'importe où -- `discSearch`, `discSkipForward`, un pas, un embranchement vers un autre segment -- et la bonne ligne est affichée à l'image dessinée suivante, en avant comme en arrière, sans rien recharger ni remettre aucune horloge à zéro. Dans un jeu à embranchements de la leçon seize, où les choix du joueur font sauter le disque sans arrêt, c'est la différence entre ce qui marche et ce qui marche presque. Le système dessine aussi au bon endroit. Le document des sous-titres se place par-dessus l'image, au-dessus de votre overlay, en dessous de toute GUI en espace écran, et il suit l'image partout où les réglages du joueur la mettent -- une image réduite, une image décalée, une borne tournée, un trou dans un bezel. Il ne prend aucune entrée de la souris, donc un tir de pistolet optique le traverse sans s'arrêter. Réussir tout ça vous-même, c'est une semaine que vous n'aviez pas prévue. [#_write_the_file] === Écrivez le fichier Les sous-titres vont dans un fichier SubRip, qui se termine par `.srt` et qui est du texte brut que vous pouvez taper dans n'importe quel éditeur. C'est l'un des formats les plus simples qui existent. Créez un fichier appelé `subtitles.srt` à côté de votre script : ---- 1 00:00:01,000 --> 00:00:04,000 They told us the cave was empty. 2 00:00:04,200 --> 00:00:08,000 They were wrong about a great many things. 3 00:00:08,500 --> 00:00:13,000 Keep your light on the floor. ---- Un sous-titre, c'est quatre choses. Un numéro seul sur sa ligne. Une ligne avec deux horodatages et, entre eux, une flèche faite de deux traits d'union et d'un signe supérieur. Une ou plusieurs lignes de texte. Une ligne vide pour finir. Un horodatage s'écrit `HH:MM:SS,mmm` -- les heures, les minutes et les secondes séparées par des deux-points, puis une virgule, puis les millisecondes. La virgule n'est pas une faute de frappe, et un point ne fera pas l'affaire. `00:00:04,200`, c'est quatre secondes et un cinquième après le début. Les numéros des sous-titres sont lus puis jetés, donc ils n'ont pas besoin d'être dans l'ordre, ni même d'être justes. Numérotez-les quand même ; un jour, vous chercherez le sous-titre quarante et un. Le sous-titre deux montre à quoi ressemble un sous-titre de deux lignes. Les lignes sont affichées comme vous les avez écrites, donc c'est *vous* qui décidez où une longue ligne se coupe, ce qui compte plus que vous ne le croiriez et qui est expliqué plus bas. L'analyseur est volontairement tolérant envers ce qui tourne mal dans un fichier modifié à la main. Les fins de ligne Windows se chargent telles quelles, et les espaces au début et à la fin de chaque ligne sont retirés. Un sous-titre dont les horodatages ne s'analysent pas est sauté et le reste du fichier continue de se charger, ce qui est sympathique jusqu'au moment où vous vous demandez où est passée la ligne douze. Quelques choses ne sont pas prises en charge : les balises `` et `` que certains outils écrivent sont affichées comme du texte au lieu d'être appliquées, et les coordonnées de positionnement que certains outils ajoutent à la ligne de temps sont ignorées. La section Sous-titres de la Référence en donne la liste complète. [#_load_it_and_switch_it_on] === Chargez-les et allumez-les Trois appels, et l'ordre compte. [source,lua] ---- if srtLoad(DIR .. "subtitles.srt") then srtPosition(80) srtEnable(true) end ---- `srtLoad` lit le fichier et répond `true` ou `false`. Elle répond `false`, sans rien avoir chargé, dans trois cas : le fichier ne peut pas être lu, rien dedans ne s'analyse, ou *il n'y a pas de disque*. Dans les trois cas, elle affiche à la console un avertissement qui nomme le fichier. C'est une bonne conception et vous devriez vous en servir : un fichier de sous-titres manquant ne vaut pas qu'on tue un jeu pour ça, mais livrer sans s'en apercevoir un jeu sans sous-titres vaut qu'on le remarque. Le cas « pas de disque » est celui qui vous attrapera. Les horodatages doivent devenir des numéros d'image à la fréquence d'images du disque, et tant qu'aucun disque n'est ouvert, il n'y a pas de fréquence d'images à utiliser. Chargez donc les sous-titres *après* le disque, ce qui, dans un script ordinaire, veut dire depuis le corps du script, là où s'exécute tout ce que vous avez écrit au niveau principal. Il s'ensuit qu'un `.srt` découpé pour une vidéo à une certaine fréquence d'images tombe au mauvais endroit sur une vidéo à une autre. Synchronisez votre fichier sur la vidéo que vous allez vraiment livrer. `srtEnable(true)` allume les sous-titres chargés. Un script démarre toujours avec les sous-titres éteints, donc un jeu qui les charge doit les allumer. Les éteindre de nouveau retire ce qui est affiché, et les rallumer affiche ce qui couvre l'image sur laquelle se trouve le disque à cet instant -- rien à chercher, aucun état à garder. Laisser les sous-titres allumés est un réglage, pas une décision que vous prenez à la place du joueur. Mettez-le dans votre page d'options de la leçon vingt, enregistrez-le avec `saveSet` de la leçon douze, et passez à `srtEnable` ce que le joueur a choisi en dernier. [#_where_they_sit] === Où ils se placent [source,lua] ---- srtPosition(70) ---- `srtPosition` est un pourcentage de la hauteur de l'image, compté depuis le haut, de `1` à `95`, et la valeur par défaut est `80`. Il s'applique au sous-titre affiché à cet instant et à tous ceux qui suivent. Un nombre hors de cette plage est ignoré au lieu d'être ramené dans les limites, ce qui veut dire qu'une faute de frappe laisse la position exactement où elle était, sans rien dire, alors vérifiez les vôtres. Quatre-vingts pour cent, c'est bas, et c'est bas exprès : le bas de l'image est en général la partie la moins intéressante du plan. Remontez-les quand le bas n'est *pas* ennuyeux -- quand votre jeu dessine les scores le long du bord inférieur, ou quand le réticule du pistolet optique du joueur vit par là. [#_taking_one_down] === En retirer un [source,lua] ---- srtClear() ---- `srtClear` retire immédiatement ce qui est à l'écran, et laisse tranquilles les sous-titres chargés et l'état allumé. La ligne qui était affichée reste retirée jusqu'à ce que le disque atteigne le sous-titre suivant. Servez-vous-en quand une scène se termine plus tôt et que vous ne voulez pas que la dernière réplique traîne sur le plan suivant. Un autre appel partage la mécanique des sous-titres, et vous vous en êtes peut-être déjà servi : [source,lua] ---- overlayBanner("Extra life!", 20) ---- `overlayBanner` affiche un court message par-dessus l'image pendant une trentaine d'images dessinées, puis le retire. Il est dessiné par le même document que les sous-titres, donc un bandeau ressemble à un sous-titre, et tant qu'il est affiché il prend la place de tout sous-titre, qui revient quand le bandeau expire. Son propre argument de hauteur est distinct de `srtPosition` et vaut `47` par défaut, le milieu de l'image. Les messages de plus de soixante caractères sont purement et simplement ignorés, donc un message long a l'air de ne pas marcher du tout. [#_subtitles_already_inside_the_video] === Des sous-titres déjà dans la vidéo Certains fichiers vidéo transportent leurs propres pistes de sous-titres. Si c'est le cas du vôtre, vous n'avez pas du tout besoin d'un `.srt` : [source,lua] ---- if discGetSubtitleTracks() > 0 then srtLoadTrack(0) else srtLoad(DIR .. "subtitles.srt") end srtEnable(true) ---- `srtLoadTrack` prend un numéro de piste de `0` à `discGetSubtitleTracks() - 1` et, une fois le chargement fait, tout est identique. `discGetSubtitleLanguage(track)` vous dit dans quelle langue une piste est étiquetée, sous la forme d'un code de trois lettres comme `eng` ou `fra`, pour qu'un jeu puisse proposer au joueur ce que sa vidéo se trouve transporter. Deux mises en garde. Lire une piste parcourt une fois tout le fichier vidéo, sans décoder aucune image, donc un disque de la longueur d'un long métrage prend un moment : faites-le pendant que quelque chose d'autre est à l'écran, pas entre deux images de jeu. Et les pistes de sous-titres qui contiennent des *images* des mots au lieu des mots -- le genre qu'utilisent les DVD et les Blu-ray -- ne peuvent pas être lues. Elles sont comptées, pour que la numérotation corresponde à ce que montrent les autres lecteurs, mais `srtLoadTrack` répond `false` pour elles. [#_the_whole_script_3] === Le script complet Celui-ci a besoin d'une vidéo. Vous en avez une : `Singe/menuBackground.mkv`, que le moteur a extraite dans votre dossier de travail la première fois que vous avez lancé quoi que ce soit. Dites à Singe de s'en servir comme disque avec `--framefile`, qui allume aussi le disque : ---- Singe --framefile=Singe/menuBackground.mkv subtitles ---- [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local HUD_X = 16 local STEP = 5 local TOP_LIMIT = 10 local LOW_LIMIT = 95 local hudFont = fontLoad("Singe/FreeSansBold.ttf", 18) local loaded = false local showing = false local height = 80 if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end if loaded then srtPosition(height) showing = true srtEnable(true) end discPlay() function onInputPressed(what) if not loaded then return end if what == SWITCH_BUTTON1 then showing = not showing srtEnable(showing) if showing then overlayBanner("Subtitles on", 20) else overlayBanner("Subtitles off", 20) end elseif what == SWITCH_UP then height = math.max(TOP_LIMIT, height - STEP) srtPosition(height) elseif what == SWITCH_DOWN then height = math.min(LOW_LIMIT, height + STEP) srtPosition(height) elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() end end function onOverlayUpdate() overlayClear() fontSelect(hudFont) colorForeground(255, 255, 255) if loaded then if showing then fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles ON at " .. height .. "%") else fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles OFF") end fontPrint(HUD_X, 40, "Fire toggles them, up and down move them, left rewinds") else fontPrint(HUD_X, 16, "No subtitles loaded.") fontPrint(HUD_X, 40, "Run with --framefile and check subtitles.srt is beside the script.") end return OVERLAY_UPDATED end function onShutdown() fontUnload(hudFont) end ---- Appuyez sur tir pour les allumer ou les éteindre, sur haut et bas pour déplacer les lignes, et sur gauche pour revenir au début. C'est le retour au début qu'il faut regarder : le disque saute à l'image zéro et le bon sous-titre est affiché dès l'image suivante, sans que vous ayez rien fait pour ça. [#_what_just_happened_17] === Ce qui vient de se passer [source,lua] ---- if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end ---- Prenez les sous-titres de la vidéo elle-même quand elle en a, rabattez-vous sur le fichier à côté du script quand elle n'en a pas, et finissez en sachant lequel des deux a marché -- si l'un des deux a marché. Les deux appels répondent par un booléen, donc le repli tient en deux instructions `if`, sans cas particulier. `menuBackground.mkv` ne transporte aucune piste de sous-titres, donc ce que vous obtiendrez réellement, c'est le fichier que vous avez tapé. [source,lua] ---- function onInputPressed(what) if not loaded then return end ---- Un `return` sans rien après quitte la fonction immédiatement. Sans sous-titres chargés, aucune de ces touches n'a rien à faire, et partir tôt est plus clair que d'envelopper tout le corps dans un `if`. Appeler `srtEnable` sans rien de chargé est sans danger, mais écrire du code qui n'a de sens que lorsqu'il ne fait rien est une habitude qu'il vaut mieux ne pas prendre. [source,lua] ---- elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() ---- Le `srtClear` n'est pas nécessaire. Le moteur affichera de toute façon le bon sous-titre pour l'image zéro à l'image dessinée suivante. Il est là pour que vous puissiez le supprimer et regarder ce qui se passe : l'ancienne ligne reste affichée une fraction de seconde pendant que le disque arrive là où il va. Sur un saut lent, ce flash de la mauvaise ligne se voit, et effacer d'abord est la façon de l'éviter. [#_making_text_readable_over_a_moving_picture] === Rendre le texte lisible sur une image qui bouge Le système met les mots à l'écran. Que quelqu'un arrive à les lire, c'est votre problème, et ça vaut pour chaque ligne de texte que vous dessinez par-dessus de la vidéo -- une consigne de la leçon dix-huit, un score, un choix d'embranchement -- pas seulement pour les sous-titres. *Restez loin des bords.* Les téléviseurs coupent les bords des images depuis soixante-dix ans, et les moniteurs d'arcade sont pires, pas meilleurs. Partez du principe que les cinq pour cent extérieurs de l'image, de chaque côté, peuvent ne pas exister du tout sur l'écran de quelqu'un. Gardez à l'intérieur tout ce qu'un joueur doit lire. C'est pour ça que `srtPosition` s'arrête à `95` et pas à `100`. *Le contraste n'est pas un choix de couleur, c'est une garantie.* Du texte blanc sur une image, c'est du texte blanc sur ce que l'image est en train de montrer, et dans deux secondes ce sera peut-être un champ de neige. Le style de sous-titres fourni règle ça comme le font les sous-titres de cinéma : un panneau sombre qui épouse le texte, plus une ombre d'un pixel. Si vous dessinez votre propre texte sur de la vidéo, faites l'un ou l'autre -- un `overlayBox` derrière, ou une copie sombre du texte décalée d'un pixel. Ne comptez pas sur l'image. *Coupez vos lignes vous-même.* Deux lignes courtes se lisent plus vite qu'une longue, et une ligne qui passe à la ligne là où vous ne l'avez pas choisi se coupe en général au mauvais endroit. Visez une quarantaine de caractères par ligne et pas plus de deux lignes à la fois. Coupez à une pause naturelle : `They were wrong about / a great many things` se lit bien ; `They were wrong / about a great many things` trébuche. *Laissez-les assez longtemps.* Une vitesse de lecture confortable tourne autour de quinze caractères par seconde, donc une ligne de quarante caractères demande environ trois secondes. Regardez le sous-titre un dans le fichier ci-dessus : trente-deux caractères, trois secondes. Si une réplique est courte et rapide, garder le sous-titre affiché un temps de plus après la fin de la parole est plus gentil que de le faire disparaître d'un coup. *Mettez-les là où l'action n'est pas.* Si ce qui compte dans le plan se trouve en bas de l'image, remontez les sous-titres pour cette scène avec `srtPosition`. Vous pouvez l'appeler aussi souvent que vous voulez, et le joueur ne remarquera jamais que vous l'avez fait. [#_styling_them] === Leur mise en forme Les lignes sont dessinées par un document, du même genre que ceux de la leçon vingt, et deux fichiers de votre dossier `Singe` décident de leur apparence : * `Singe/subtitle.rml` -- le document. Il contient un seul élément, `#slot`, dans lequel le moteur écrit le sous-titre ou le bandeau du moment. * `Singe/subtitle.rcss` -- le style. `.cue` est le bloc qui porte la hauteur demandée par `srtPosition`, et `.cue .text` est la barre de texte à l'intérieur : blanc, gras, sur un panneau sombre arrondi qui épouse le texte, avec une ombre. Ouvrez `subtitle.rcss` et lisez-le -- il fait une quarantaine de lignes. Puis laissez-le tranquille, parce qu'une modification faite là ne dure pas. À chaque démarrage, Singe remet sa propre copie de tout ce qui a été modifié dans le dossier `Singe`, et le dit : ---- >>> Updated File: Singe/subtitle.rcss ---- Ce dossier appartient au moteur. Votre jeu apporte plutôt sa propre copie. Copiez les deux fichiers depuis `Singe` dans votre dossier `subtitles`, à côté du script. Puis ouvrez votre copie de `subtitle.rml` et changez la seule ligne de son `` qui relie le style, pour qu'elle se lise : [source,html] ---- ---- Un nom à l'intérieur d'un document part du dossier du document lui-même, sauf un nom qui commence par `Singe/`, qui désigne toujours le dossier du moteur. Le lien tel que vous l'avez copié, `Singe/subtitle.rcss`, va toujours chercher le style du moteur. Le nouveau va chercher le vôtre. Quand Singe trouve un `subtitle.rml` à côté de votre script, il dessine les sous-titres avec celui-ci au lieu du sien. Ajoutez `-p` à la commande et Singe décrit ce qu'il fait au fur et à mesure, une ligne à la fois : ---- Singe -p --framefile=Singe/menuBackground.mkv subtitles ---- Quand le premier sous-titre apparaît, l'une de ces lignes est celle-ci : ---- Using the game's own subtitles/subtitle.rml ---- Changez maintenant la `font-size`, la `color`, la `background-color`, le `border-radius` ou le `font-effect` dans votre `subtitle.rcss`, enregistrez, et relancez. Le moteur ne touche jamais qu'au contenu de `#slot` et à la hauteur de `.cue`, donc tout le reste de vos deux fichiers est à vous, et rien de ce que fait Singe à son démarrage n'y touchera. Si `-p` dit qu'il utilise les vôtres et que l'apparence n'a pas changé, relisez le lien : une copie qui dit encore `Singe/` utilise en silence le style du moteur. Comme `overlayBanner` utilise le même document, changer le style des sous-titres change aussi celui de vos bandeaux, ce qui est en général ce que vous voulez et qui vaut d'être su avant que vous vous demandiez pourquoi. Une limite : le document des sous-titres est un document, et les documents ont besoin d'un GPU. Sur une machine qui n'en a pas, `srtLoad` lit quand même votre fichier et répond quand même honnêtement, et rien n'est dessiné. Un avertissement le dit une fois. [#_try_it_21] === Essayez . *Ajoutez un quatrième sous-titre* à `subtitles.srt`, entre onze et treize secondes. Enregistrez et lancez. Puis mettez-le dans le désordre, en tant que sous-titre `99`, en premier dans le fichier, et vérifiez qu'il marche toujours. . *Donnez à un sous-titre un mauvais horodatage* -- écrivez `00:00:05.500` avec un point au lieu d'une virgule -- et lancez. Regardez quelle ligne disparaît, et remarquez que rien du tout n'est dit à ce sujet. Un fichier qui se charge n'est pas un fichier juste. . *Changez leur style.* Dans votre propre `subtitle.rcss` -- la copie qui est à côté de votre script, pas celle de `Singe` -- changez `.cue .text` pour avoir du texte jaune sur un fond complètement transparent, puis ajoutez une ombre plus marquée. Ensuite, regardez-le par-dessus la vidéo et décidez honnêtement s'il est encore lisible. . *Cassez la synchronisation exprès.* Changez chaque horodatage du fichier pour qu'il soit en retard de deux secondes, et regardez un système qui marche parfaitement livrer des sous-titres inutiles. Voilà ce que vous fait une vidéo réencodée. . *Mettez l'interrupteur dans un menu.* Reprenez la page de pause de la leçon vingt, ajoutez une case à cocher, et branchez-la sur `srtEnable`. Puis enregistrez le choix avec `saveSet` pour qu'il soit encore allumé la fois suivante. C'est la version que vos joueurs veulent vraiment. [#_break_it_on_purpose_20] === Cassez-le exprès Lancez le script sans l'option `--framefile` : ---- Singe subtitles ---- La fenêtre s'ouvre, noire, avec votre message de repli dessus, et la console dit : ---- Warning: No subtitles were loaded from subtitles/subtitles.srt. ---- Pas de plantage et pas de boîte de dialogue d'erreur, parce que `srtLoad` a répondu `false` et que votre script s'en est occupé. C'est l'échec le plus précieux de cette leçon, parce que c'est celui qui arrive sans que personne ne le remarque. Il n'y a pas de disque, donc pas de fréquence d'images, donc rien en quoi transformer les horodatages, donc rien ne se charge. Si vous aviez écrit `srtLoad(...)` tout seul en jetant la réponse, votre jeu aurait tourné parfaitement sans montrer aucun sous-titre, et vous l'auriez découvert quand quelqu'un vous l'aurait dit. Servez-vous de la réponse. À chaque fois. [#_what_you_learned_21] === Ce que vous avez appris * Un jeu avec des paroles et sans sous-titres est un jeu auquel certaines personnes ne peuvent pas jouer du tout. * Mettre les sous-titres au fur et à mesure, c'est un après-midi ; les ajouter après coup, non. * Les sous-titres sont un fichier `.srt` : un numéro, deux horodatages avec `-->` entre eux, le texte, et une ligne vide. * `srtLoad` transforme les horodatages en numéros d'image du disque au chargement, et c'est pour ça qu'un sous-titre survit à n'importe quel saut. * Il faut charger les sous-titres après le disque, et l'appel répond `false` quand il ne peut pas charger, y compris quand il n'y a pas de disque. Vérifiez la réponse. * `srtEnable` les allume, et un script démarre toujours avec les sous-titres éteints. * `srtPosition` les place à un pourcentage de la hauteur de l'image, de `1` à `95`. * `srtClear` retire ce qui est affiché, et `overlayBanner` partage le même document. * `srtLoadTrack` lit les sous-titres dans le fichier vidéo lui-même quand il en contient. * Singe réécrit son dossier `Singe` à chaque démarrage, donc pour changer le style des sous-titres, vous copiez `subtitle.rml` et `subtitle.rcss` à côté de votre script, vous faites pointer le lien de la copie vers votre propre feuille, et c'est celle-ci que vous modifiez. * Du texte sur une image qui bouge a besoin d'une marge de sécurité, de son propre contraste, de lignes coupées par vous, et de temps pour être lu. [#_next_time_21] === La prochaine fois C'en est fini du texte. La quatrième partie continue sans que vous ayez à la lire dans l'ordre, et la prochaine leçon ouvre une porte restée fermée pendant vingt leçons : `overlayGetWidth` et `overlayGetHeight` sont deux nombres, et une scène en a trois. La leçon vingt-deux parle du troisième.