Aller au contenu

Lire des dialogues dans des interfaces 2D

Un Dialogue n'a pas besoin d'être joué comme une scène. Il peut aussi être lu par une interface 2D de votre cru, quelle que soit sa forme : une lettre ou une page de terminal, un fil de messagerie sur un téléphone, une popup avec un choix, la transcription d'une messagerie vocale, un écran de verrouillage qui attend un code, pour n'en citer que quelques-unes. Cette page explique comment : les helpers de lecture qui parcourent un Dialogue en dehors du Sequencer, le flag Staging.Interface2D, les choix à saisie et le reply par défaut, ainsi que le lecteur 2D, les vues et le device d'exemple livrés avec le showcase.

Pour la version jouée (stages, 3C, cinématiques, StartDialogue*), voir Jouer des dialogues. Pour des exemples concrets dont vous inspirer, voir la FAQ interfaces 2D.

Ce qui fait partie du produit, et ce qui est un exemple

Couche Où Ce qu'elle vous apporte
Helpers de lecture Produit, ULBRDialogueReadingLibrary (module LBRCore) La sémantique Libretto d'un Dialogue sans le Sequencer : quel sous-dialogue se joue, l'étape suivant une ligne, If/Else, Goto, replies valides, reply par défaut, saisie, effets de ligne, flags « done ».
Flag Staging.Interface2D Produit Marque un sous-dialogue comme lu par une interface 2D : aucune séquence ni aucun stage ne sont générés, les voix le sont toujours.
Lecteur 2D, contrat de vue, vues, device Exemples, module LibrettoExamplesRuntime Une manière de construire des interfaces 2D sur les helpers. Lisez-la, copiez-la, remplacez-la : l'UI de votre jeu vous appartient.

Les helpers sont la partie sur laquelle vous vous appuyez. Le lecteur et les vues d'exemple sont là pour montrer une implémentation complète, et sont faits pour être lus plutôt qu'étendus.

Le flag Staging.Interface2D

Cochez Libretto.Flags.Subdialogue.Staging.Interface2D sur chaque sous-dialogue lu par une interface 2D.

  • La génération l'ignore : pas de sous-séquence, pas de binding de stage, pas de caméra. Ses assets audio sont toujours générés (voix importées, text to speech), si bien qu'une messagerie vocale peut faire entendre une vraie voix.
  • Un dialogue joué n'y entre jamais : une Dialogue Instance ne choisit pas un sous-dialogue Interface2D, et un saut Sequencer vers l'un d'eux termine le dialogue.
  • Un dialogue dont tous les sous-dialogues sont Interface2D (ou Audio2D) est générique : il n'a besoin d'aucune instance placée.
  • Comptage de mots : ses lignes sont exclues des mots parlés, et conservées dans les mots localisables.
  • Il ne se combine pas avec 3C, CinematicDialogue, RandomBarks ou Audio2D sur le même sous-dialogue. La combinaison est signalée à la compilation du Dialogue, et l'asset se sauvegarde quand même.

Les flags enfants sont à vous de définir, un par type d'interface. Les exemples déclarent Interface2D.Document, .Messaging, .Voicemail, .Popup et .Lock, plus le flag de ligne Libretto.Flags.Lines.Interface2D.Title (la ligne utilisée comme titre de page). Filtrer sur Interface2D accepte tous ses enfants.

Les helpers de lecture

ULBRDialogueReadingLibrary est une bibliothèque de fonctions Blueprint sans état (catégorie Libretto ▸ Reading). Quelques règles valent pour toutes les fonctions :

  • Une ligne est désignée par son index dans les tableaux de lignes du Dialogue. ULBRDialogue::GetIndex(ID) convertit un ID de ligne en index.
  • Filtre de flags. Les fonctions qui prennent un FlagFilter n'acceptent que les sous-dialogues qui portent ce flag (hiérarchiquement). Un filtre vide accepte tous les sous-dialogues.
  • Les conditions sont évaluées avec leur cache (EvaluateWithCache, jamais forcé) : une condition avec bCacheSuccess ou bCacheFailure fige son résultat la première fois qu'elle est affichée. Voir Conditions — cache.
  • Aucun flag « done » n'est levé pour vous. C'est votre interface qui décide quand une page compte comme lue ou un fil comme terminé, et qui appelle MarkSubdialogueDone ou MarkDialogueDone.
  • Des données invalides ne plantent jamais. Un dialogue nul ou un index hors limites renvoie un résultat Invalid et journalise un warning.
Fonction Renvoie Rôle
FindFirstPlayableSubdialogue(Dialogue, FlagFilter) Subdialogue Le premier sous-dialogue, dans l'ordre de l'asset, qui passe le filtre et sa condition.
GetPlayableSubdialogues(Dialogue, FlagFilter) Tableau Tous les sous-dialogues jouables, dans l'ordre : une table des matières, la liste des fils d'un téléphone.
GetSubdialogueEntry(Dialogue, Subdialogue, FlagFilter) FLBRReadStep La première étape d'un sous-dialogue (commentaires et lignes If sautés).
GetNextStep(Dialogue, FromLineIndex, FlagFilter) FLBRReadStep L'étape après une ligne affichée, ou après un reply choisi (elle entre dans le corps du reply). Gère If/ElseIf/Else, la fin d'un reply, Goto -1 (fin), -2 (sous-dialogue jouable suivant) et Goto vers un ID.
GetValidReplies(Dialogue, ChoiceIndex) Tableau de FLBRReplyOption Les replies à proposer : les replies DoOnce déjà choisis et ceux dont la condition Display échoue sont retirés ; bAvailable vaut false quand la condition Availability échoue (affichez-le grisé) ; bIsDefault marque le reply par défaut.
GetDefaultReply(Dialogue, ChoiceIndex) Index de ligne Le reply par défaut, s'il est encore valide. INDEX_NONE sinon.
FindReplyMatchingInput(Dialogue, ChoiceIndex, Input, bIsDefault) Index de ligne Le reply dont le texte correspond à ce que le joueur a saisi, ou le reply par défaut (bIsDefault vrai). Voir Saisie.
RecordReplyChosen(Dialogue, ReplyIndex) — Enregistre le reply dans les GPVars, exactement comme un dialogue joué.
GetRetainedReply(Dialogue, ChoiceIndex, ReplyIndex) None / Single / Ambiguous Quel reply d'un choice a été choisi auparavant : aucun, exactement un, ou plusieurs.
ApplyLineEffects(WorldContext, Dialogue, LineIndex) — Applique les SetTags de la ligne, puis ses RemoveTags, puis ses actions. Toutes les actions s'exécutent, même si l'une d'elles lance un autre dialogue.
MarkSubdialogueDone(Dialogue, Subdialogue) / MarkDialogueDone(Dialogue) — Lève les mêmes flags « done » qu'un dialogue joué, si bien que ULBRDialogueDoneCondition les voit.

Une étape (FLBRReadStep) vous dit quoi faire ensuite :

Kind Signification Champs
Line Afficher cette ligne (Text, Cinematic, Silence). LineIndex, Subdialogue
Choice Présenter ce choice et attendre le joueur. LineIndex, Subdialogue
End La lecture s'arrête. EndReason

Subdialogue change quand un Goto passe dans un autre sous-dialogue. EndReason vaut EndOfSubdialogue (Goto -1 ou fin naturelle), OutOfFilter (un saut vers un sous-dialogue qui ne passe pas le filtre, comme un sous-dialogue 3D joué), InvalidDestination (un Goto vers un ID inconnu), StepBudget (un cycle de Goto) ou Invalid.

La boucle de lecture

Avec ces helpers, un lecteur tient en une courte boucle. Voici ce que fait le lecteur d'exemple :

  1. Choisir le sous-dialogue : FindFirstPlayableSubdialogue(Dialogue, Interface2D), ou celui que votre UI a choisi.
  2. Obtenir sa première étape avec GetSubdialogueEntry.
  3. Tant que l'étape est une Line : l'afficher, appeler ApplyLineEffects, puis GetNextStep depuis cette ligne.
  4. Sur un Choice : afficher GetValidReplies et attendre. Quand le joueur choisit un reply, appeler RecordReplyChosen, puis ApplyLineEffects sur le reply, puis continuer avec GetNextStep depuis le reply.
  5. Sur un End : fermer la page, le fil ou la popup.

Gardez les règles de Libretto hors de votre UI

Votre boucle ne devrait regarder que FLBRReadStep. Si vous vous surprenez à lire un chemin de GPVar, le Type d'une ligne ou une condition directement, un helper fait déjà ce travail pour vous.

Saisie et reply par défaut

Un écran de verrouillage, un mot de passe ou une énigme est un Choice dont les replies sont les réponses acceptées. Le joueur tape, et FindReplyMatchingInput trouve le reply :

  • il compare avec les replies proposés et disponibles, reply par défaut exclu ;
  • les espaces en début et en fin sont ignorés, la casse aussi. Les accents comptent : café n'est pas cafe ;
  • le texte du reply est comparé dans la culture courante, si bien qu'un mot de passe traduit se tape dans la langue jouée ;
  • quand rien ne correspond, il renvoie le reply par défaut avec bIsDefault vrai, ou INDEX_NONE s'il n'y en a pas.

Le reply par défaut (Default Reply sur un Reply) est le reply pris quand le joueur ne donne aucune réponse valide : le temps d'un choix minuté s'écoule, ou la saisie ne correspond à rien. Il peut donc aussi être posé sur un choix non minuté. Il y en a au plus un par Choice.

Default Reply sur un choix non minuté

Piège : un reply par défaut est DoOnce par défaut

Comme tout reply, un reply par défaut a bDoOnceReply coché par défaut. Une fois pris, il n'est plus valide : le deuxième code faux ne trouve aucun repli, et FindReplyMatchingInput renvoie INDEX_NONE. Pour un verrou ou une énigme que le joueur peut rater plusieurs fois, décochez DoOnce sur le reply par défaut.

Le lecteur 2D d'exemple

Le module d'exemples livre un lecteur, ULBRInterface2DReader, et un contrat de vue, ILBRInterface2DView. Le lecteur parcourt le Dialogue avec les helpers ; la vue affiche ce qu'elle reçoit et décide du rythme (délais, « en train d'écrire… », une page affichée d'un coup).

Le lecteur avance jusqu'à la prochaine étape bloquante, un choice ou une fin. Chaque ligne est envoyée à la vue, puis ses effets sont appliqués dans la même frame. Si une action ferme la vue, la lecture s'arrête proprement.

Lecteur → vue (BlueprintNativeEvent) Envoyé quand
EnterSubdialogue(Subdialogue) La lecture entre dans un sous-dialogue. Une vue racine choisit sa sous-vue d'après les flags.
ShowText(Line) Une ligne est affichée. FLBRInterface2DLine porte Text, SpeakerName, Flags, TypedActions (indications de présentation, comme une image), Audio (jamais joué par le lecteur) et bReplay.
PresentReplies(ChoiceIndex, Options, TimeLimit) Un choice attend. TimeLimit vaut 0 pour un choix non minuté.
ReplyChosen(ReplyIndex, bReplay) Un reply a été choisi, p. ex. pour afficher le message envoyé.
Unlocked() / NoMatch() La saisie correspond à une réponse / ne correspond à rien.
Finished(Reason) La lecture s'est arrêtée d'elle-même.
Vue → lecteur Effet
Start(Dialogue, Subdialogue, View) Entre dans le sous-dialogue (le premier Interface2D jouable s'il est nul) et lit.
Reconstruct(Dialogue, Subdialogue, View) Rejoue ce qui a déjà été lu, puis continue en direct. Voir Reconstruction.
ChooseReply(ReplyIndex) Enregistre le reply, applique ses effets et lit son corps.
SubmitInput(Text) Une réponse correcte envoie Unlocked, marque le sous-dialogue du verrou comme terminé et prend le reply. Le reply par défaut envoie NoMatch et est pris. Aucune correspondance du tout envoie NoMatch et représente le choice.
TimeOut() Prend le reply par défaut s'il y en a un ; sinon le choice continue d'attendre.
Stop() S'arrête sans Finished, p. ex. quand la vue se ferme.

Le lecteur ne lève aucun flag « done », sauf pour un verrou ouvert. Ce sont les vues qui lèvent les autres, chacune selon son usage.

Actions typées : indications de présentation

Une ligne peut porter des actions qui ne font rien par elles-mêmes (DoAction est vide) mais indiquent à la vue comment la présenter. La vue les lit dans TypedActions :

  • Interface2D Image (ULBRI2DImageAction) : une image affichée avec la ligne ;
  • Interface2D Play Audio Subdialogue (ULBRI2DPlayAudioSubdialogueAction) : un sous-dialogue Audio2D du même Dialogue, joué quand le joueur appuie sur Play (une messagerie vocale).

Leurs propriétés ne sont pas editor-only : elles sont lues dans un jeu packagé.

Reconstruction

Un fil de messagerie montre ce qui a déjà été dit à chaque réouverture du téléphone. Libretto ne garde aucune transcription : le fil est reconstruit à partir des GPVars, qui enregistrent déjà chaque reply choisi.

Reconstruct lit le fil avec bReplay levé. Il n'applique aucun effet, ne lève aucun flag « done » et ne joue aucun audio ; il envoie quand même les indications de présentation. À chaque choice :

  • si un seul reply a été choisi auparavant (GetRetainedReply renvoie Single) et que le choice n'a pas encore été rencontré pendant cette reconstruction, le lecteur envoie ReplyChosen(bReplay) et le suit ;
  • sinon (jamais répondu, plusieurs replies choisis, ou rencontré une deuxième fois), la relecture s'arrête et le choice est présenté en direct.

Deux règles en découlent, et elles comptent quand vous écrivez un fil :

  • Un fil ne passe pas deux fois par le même choice. Un fil qui reboucle sur un choice cesse d'être reconstruit au deuxième passage : le joueur y répond à nouveau.
  • Les conditions d'un fil devraient être en cache. Une condition est réévaluée pendant la reconstruction. Si son résultat a pu changer depuis la lecture du fil, le fil reconstruit prend une autre branche. Cochez bCacheSuccess / bCacheFailure sur les conditions qui décident de ce qu'un fil a déjà dit.

Les vues et le device d'exemple

Les vues sont des classes C++ UUserWidget qui construisent leur mise en page en code, dans LibrettoExamplesRuntime/Interface2D/. Elles partagent une base, ULBRI2DView (cadre, titre, bouton de fermeture, Open(Dialogue, Subdialogue), OnClosed, OnFinished).

Vue Usage Marqué terminé par la vue
ULBRI2DDocumentView Table des matières (les sous-dialogues Document, titrés par leur ligne Title), une page affichée d'un coup, des images. La page, à son ouverture.
ULBRI2DPhoneView Entre sur le verrou s'il n'est pas encore ouvert, sur l'écran d'accueil sinon. Tuiles pour les fils et la messagerie vocale. —
ULBRI2DLockView Champ de saisie et retour NoMatch / Unlocked. Levé par le lecteur.
ULBRI2DMessagingView Un fil au nom de son locuteur, reconstruit à l'ouverture, avec des délais de frappe. Le fil, quand il se termine.
ULBRI2DVoicemailView La transcription et un bouton Play. La messagerie vocale, quand son audio se termine.
ULBRI2DPopupView Un texte et un choix minuté. Les sous-dialogues lus, à la fin.

ALBRI2DDevice ouvre une vue sur un Dialogue. C'est un trigger de showcase utilisé avec la touche Interact : il tient une référence directe au Dialogue (pour que le cook le suive) et une View Class. C'est l'appelant qui choisit la vue : le Dialogue ne dit rien de la manière dont il est affiché. Le device donne l'input à l'UI tant que la vue est ouverte, et le rend au jeu à sa fermeture.

Sauvegarde

Tout ce dont une interface 2D a besoin pour se rouvrir dans le même état tient déjà dans deux objets : les GPVars (replies choisis, flags « done », vos propres tags) et l'historique des conditions (résultats de conditions figés). Sauvegardez les deux avec votre sauvegarde, et le document, les fils et le verrou reviennent tels que le joueur les a laissés. Voir Sauvegarder et restaurer l'état de Libretto.

Essayez : la station « 2D Interfaces »

La station « 2D Interfaces » de la map tutoriel L_LibrettoTutorial place les trois dialogues d'exemple sur trois devices :

Dialogue Device Montre
D_I2D_Document Document Des pages avec une image, un embranchement selon que le téléphone est déverrouillé, et un addendum qui apparaît une fois la messagerie vocale écoutée.
D_I2D_Phone Téléphone Un verrou (le code est sur le panneau de la station, un indice apparaît après trois codes faux), deux fils (le second apparaît une fois la messagerie vocale écoutée), une messagerie vocale avec son sous-dialogue Audio2D.
D_I2D_Popup Terminal Un premier avis avec un choix minuté, puis une page « bon retour » qui dépend du reply choisi.

La station 2D Interfaces : les trois devices devant le panneau de la station

Ils se trouvent dans /Libretto/Examples/Interface2D/Dialogues/.

Packager une messagerie vocale

La messagerie vocale joue son audio comme un dialogue générique : aucune map ne le référence, donc le cooker n'atteint ni sa séquence générée ni ses sons. Ajoutez le dossier qui les contient à Additional Asset Directories to Cook, comme pour tout dialogue générique. Le document, les textes du téléphone et la popup n'ont besoin de rien : le device référence le Dialogue.

Voir aussi : FAQ interfaces 2D · Conditions · GPVars · Blueprint API