Data & logic¶
Je veux qu'un dialogue se souvienne de ce que le joueur a déjà fait ou dit¶
En général vous n'avez rien à câbler — Libretto enregistre déjà la progression dans le store des GPVar au fil du dialogue. Un CountedGPFlag est levé quand un dialogue ou un sous-dialogue se termine, et un flag est écrit à chaque fois qu'un reply est choisi. Pour votre propre état, écrivez vos propres GPVar depuis n'importe où.

- Jouez un dialogue une fois, puis ouvrez le widget de debug à l'exécution depuis la console avec
Libretto.GPVar.OpenDebugWidget(relancez-le pour le fermer). Vous verrez les entréesDoneet les flags par reply apparaître d'eux-mêmes — exactement ce que vous auriez créé à la main. - Pour suivre votre propre état, écrivez un GPVar depuis n'importe quel Blueprint ou C++ :
RaiseGPFlag/LowerGPFlagpour un booléen,SetGPCounter/GetGPCounterpour un entier, chacun identifié par un GPTagPath (une séquence de gameplay tags, p. ex.Characters.Galeb / Stats.Karma). - Relisez-le n'importe où avec
CheckGPFlag/GetGPCounter, ou depuis une réplique de dialogue avec une condition (voir l'entrée suivante).
Piège
Les GPVar sont détenus par le ULBRGPVarSubsystem (un sous-système engine), et non par un
acteur ou la GameInstance — un GPVar survit donc au changement de niveau et ne nécessite aucune
référence à ce qui l'a créé. Mais il n'est pas sauvegardé pour vous : il est réinitialisé à la
fin de la session sauf si vous le persistez via votre propre système de sauvegarde (voir
l'entrée suivante). Et chaque fois
que vous ajoutez un nouveau gameplay tag via les Project Settings, pensez à commiter le
Config/DefaultGameplayTags.ini résultant.
Voir aussi : GPVars & Conditions · GPVars — runtime flags written during play
Je veux sauvegarder et restaurer l'état de Libretto avec ma sauvegarde¶
Sérialisez deux objets avec une archive SaveGame : le store de GPVars (ULBRGPVarSubsystem) et
l'historique des conditions (ULBRConditionHistorySubsystem). À eux deux, ils contiennent tout ce
dont Libretto se souvient : dialogues et sous-dialogues terminés, replies choisis, vos propres GPVars
et les résultats figés des conditions en cache. Libretto ne les sauvegarde pas pour vous, et n'a
besoin de rien d'autre.
-
À la sauvegarde, écrivez les deux subsystems dans vos données de sauvegarde, en C++ :
#include "LBRGPVarSubsystem.h" #include "LBRConditionHistory.h" #include "Serialization/MemoryWriter.h" #include "Serialization/ObjectAndNameAsStringProxyArchive.h" static TArray<uint8> SaveLibrettoObject(UObject* Object) { TArray<uint8> Bytes; FMemoryWriter Writer(Bytes, true); FObjectAndNameAsStringProxyArchive Ar(Writer, true); Ar.ArIsSaveGame = true; Object->Serialize(Ar); return Bytes; } // In your save routine, next to your own data: SaveGame->LibrettoGPVars = SaveLibrettoObject(ULBRGPVarSubsystem::GetGPVarSubsystem()); SaveGame->LibrettoConditions = SaveLibrettoObject(ULBRConditionHistorySubsystem::Get()); -
Au chargement, relisez-les de la même façon, avec un
FMemoryReaderetArIsSaveGame = true. Chargez les deux, ou aucun : un historique des conditions sans les GPVars contre lesquels il a été figé donne des dialogues incohérents. - Le chargement remplace tout l'historique et les GPVars. Les GPVars sous le tag racine
Transientne sont jamais conservés : ils sont supprimés au chargement.
Piège
L'historique des conditions est global et rien n'est purgé automatiquement, pas même au
déchargement d'un niveau. Les résultats des conditions portées par des acteurs de niveau
s'accumulent dans la sauvegarde jusqu'à ce que vous les purgiez : appelez
ClearConditionsOf (un dialogue, un acteur, un package) ou ClearAll sur ce dont vous
n'avez plus besoin. Et renommer ou déplacer un asset fait perdre l'historique de ses conditions.
Voir aussi : GPVars — sauvegarde · Conditions — l'historique des conditions
Je veux qu'un reply soit masqué ou grisé jusqu'à ce qu'une situation soit remplie¶
Attachez une condition au reply — le slot Display le masque quand elle est fausse, le slot Availability l'affiche grisé et non sélectionnable. Choisissez le slot selon l'effet voulu ; les deux lisent le même état de jeu en direct.

- Sur la réplique Reply, choisissez le slot : Display Condition = le reply n'est ni proposé ni affiché quand elle est fausse ; Availability Condition = le reply est affiché mais grisé et non sélectionnable.
- Attachez une
ULBRCondition— combinez les opérateurs logiques intégrés (AND / OR / NOT / XOR) avec les vérifications de GPVar (Check GPFlags,Check GPCounter, …) pour exprimer la situation. - Pour « ce dialogue a-t-il été joué ? », utilisez la
ULBRDialogueDoneConditiondédiée plutôt que de bâtir un chemin de GPVar à la main (voir l'entrée suivante).
Piège
Ne confondez pas les deux slots — Display retire le reply, Availability ne fait que le
griser. Et n'activez le bCacheSuccess / bCacheFailure d'une condition que pour des
résultats qui ne changent jamais une fois atteints (p. ex. « tutoriel terminé ») ; sur un état
volatil, une réponse mise en cache est servie indéfiniment.
Voir aussi : Conditions · GPVars — Display vs Availability · GPVars — the conditions dropdown
Je veux tester « ce dialogue a-t-il déjà été joué — et combien de fois ? »¶
Utilisez les conditions « done » conscientes de l'agencement : ULBRDialogueDoneCondition pour
« joué ou non », et ULBRDialogueDoneCountedCondition pour « combien de fois ». Elles prennent
directement les tags de dialogue / sous-dialogue et savent comment Libretto classe sa comptabilité
Done — aucune plomberie de chemin de votre côté.

- Pour une vérification oui/non, attachez
ULBRDialogueDoneCondition: donnez-lui le ou les tags de dialogue ou sous-dialogue, avec le commutateurbCheckAllpour tous vs n'importe lequel quand vous en passez plusieurs. - Pour un seuil (« joué au moins 3 fois »), utilisez
ULBRDialogueDoneCountedCondition(dérivée deULBRCheckGPCounter) pour comparer le nombre de lectures. - Elles fonctionnent partout où une condition fonctionne — Reply, branche If/ElseIf, ou sous-dialogue
entier — et tout autant depuis votre propre Blueprint via
EvaluateWithCache.
Piège
Ne reconstruisez pas le chemin Done à la main. Il est stocké sous Libretto.RuntimeFlags.Done
+ le tag de dialogue/sous-dialogue (catégorie parente + feuille), et les conditions « done »
connaissent déjà cet agencement — une condition à chemin de GPVar bricolée à la main le manquera
silencieusement si l'agencement change un jour.
Voir aussi : GPVars — how GPVars link to dialogues · Conditions — dialogue-played checks
Je veux un jet de compétence qui dépense une ressource quand il réussit — une seule fois¶
Une condition est une classe sous-classable, vous pouvez donc surcharger son évaluation pour faire quelque chose dans le cadre de la vérification — comme dépenser la ressource qu'elle vient de tester — et vous appuyer sur le cache de résultat intégré pour que cela n'arrive qu'une fois. La vérification et son effet de bord vivent ensemble au même endroit au lieu d'être câblés à la main autour de la réplique.
- Écrivez la vérification comme une condition — sous-classez
ULBRConditionet surchargezEvaluatepour comparer la stat (p. ex. un GPCounter commeCharacters.Player / Stats.Lockpick) à la difficulté. - Consommez à la vérification — effectuez la dépense dans cette même surcharge d'
Evaluate, pour qu'une vérification réussie et le coût en ressource ne fassent qu'une seule étape atomique. ActivezbCacheSuccess/bCacheFailurepour que le résultat soit mis en cache et qu'Evaluatene soit pas ré-exécuté — c'est ce qui empêche de payer le coût deux fois si la réplique est revérifiée. - Le résultat est retenu à travers la partie — le résultat en cache est rangé dans le
ULBRConditionHistorySubsystem, global, qui survit aux changements de niveau. Relisez-le avecCheckHistory(1= vrai mémorisé,0= faux mémorisé,-1= aucune entrée). Sa map estSaveGame: sérialisez le subsystem avec votre sauvegarde, en plus des GPVars, et le « une seule fois » tient aussi après un chargement.
Piège
Le cache est ce qui impose le « une seule fois » — sans bCacheSuccess / bCacheFailure,
chaque revérification ré-exécute Evaluate et dépense à nouveau, alors ne placez un effet de bord
que dans une condition dont le résultat est censé être définitif. Si vous devez la réarmer, appelez
InvalidateLastResult. L'historique n'est jamais purgé automatiquement : appelez
ClearConditionsOf quand vous n'en avez plus besoin.
Voir aussi : Conditions — writing a condition · Conditions — the success/failure cache · Conditions — the condition history
Je veux réagir au moment où une valeur de jeu change, sans scruter à chaque frame¶
Accrochez-vous directement au changement : déposez un listener qui déclenche un événement à chaque changement, ou liez les delegates du sous-système pour un contrôle plus fin. Dans les deux cas vous exécutez votre vérification une seule fois, juste après que la valeur a bougé, au lieu de la tester à chaque tick.

- La méthode simple — un listener. Utilisez un acteur listener (bâti sur
UGPFlagListener/UGPCounterListener) : donnez-lui un GPTagPath et câblez son événement dans le Level Blueprint — OnFlagChanged (quels flags, et levés vs abaissés) ou OnCounterChanged (valeur précédente / nouvelle). Un seul listener peut surveiller plusieurs chemins. - La méthode bas niveau — les delegates. Liez directement le sous-système avec
BindEventToOnGPFlagChangedPerPaths/BindEventToOnGPCounterChangedPerPaths, en filtrant les chemins vous-même. Les paramètres d'événement reflètent ceux des listeners.
Piège
Avec les delegates, vous possédez le cycle de vie — vous devez appeler le Unbind…
correspondant quand le listener disparaît, sinon vous fuitez une liaison et risquez un callback vers
un objet mort. Les listeners gèrent l'enregistrement et le désenregistrement pour vous, alors
préférez-les sauf si vous avez spécifiquement besoin du contrôle bas niveau.
Voir aussi : GPVars — reacting to changes · GPVars — the runtime debug widget
Je veux ajouter mes propres cases à cocher (flags de réplique/sous-dialogue, project settings)¶
Exposez une propriété LBRGameplayTagFlags — elle liste chaque tag enfant d'une catégorie comme sa
propre case à cocher, si bien qu'ajouter une option consiste à créer un gameplay tag, pas à modifier la
propriété. C'est ainsi que Libretto expose les Flags par réplique et par sous-dialogue et les options
de génération.

- Décidez de la catégorie sous laquelle vivent les options (p. ex.
Libretto.Flags.Lines). Une propriété de flags filtrée sur ce parent propose chaque enfant comme un interrupteur. - Ajoutez une option en créant le gameplay tag correspondant sous ce parent — via Project Settings
▸ GameplayTags, une table de tags
.ini, ou nativement en C++ (LBR_DEFINE_GAMEPLAY_TAG). La nouvelle case apparaît automatiquement ; aucun changement de propriété nécessaire. - Sur votre propre propriété, filtrez-la avec
Categories(une branche fixe) ouGetCategories(une fonction qui retourne la branche quand l'ensemble valide est contextuel), et ajoutezAutoExpandFlagspour la dessiner en ligne sous forme de grille.
Piège
Deux équipes ou plugins peuvent contribuer des options à la même propriété simplement en
déclarant des tags sous le même parent — c'est piloté par la donnée, rien n'a besoin de connaître
les autres. Comme toujours, commitez Config/DefaultGameplayTags.ini après avoir ajouté des
tags, sinon les cases n'apparaîtront pas pour vos coéquipiers.
Voir aussi : Gameplay Tag Flags · Gameplay Tag Flags — filtering the options
Mes tags de dialogue sont manquants ou désynchronisés (surtout dans un build packagé)¶
Libretto génère les tags de dialogue à partir des assets eux-mêmes au chargement, donc le correctif
n'est presque jamais d'éditer un .ini à la main — c'est de laisser le scan se relancer, et de
s'assurer que le bake au moment du cook a bien été livré avec votre build. Il y a une source de
vérité unique : les assets Dialogue.
- Dans l'éditeur, les tags sont (re)générés au chargement du projet par le
ULBRDialogueTagsSubsystem, qui garde un cache éditeur dansSaved/LBRDynamicTags.cache. Si les tags d'un dialogue nouvellement ajouté ou renommé sont manquants ou périmés, supprimez ce fichier cache — le prochain chargement du projet rescanne chaque asset et régénère les tags à partir de zéro. Ne tapez jamais de tags dans un fichier de config à la main ; ils se désynchronisent des assets. - Dans un build packagé, les tags sont bakés au moment du cook dans
Config/DynamicTags/DynamicTags.ini(chargé comme chemin de recherche de tags à l'exécution) et l'index tag→dialogue dansConfig/LBRDynamicTags.ini. Si les tags ne manquent que dans le jeu packagé, vérifiez que ces fichiers ont bien été produits par le cook et inclus. -
Les sélecteurs de groupe partagés par plusieurs sous-dialogues vivent sous
Libretto.Dialogues.<DialogueName>.SubdialogueSelectors.<Name>— le même arbre généré, pas une liste séparée à maintenir.
Piège
Les Dynamic Tags réutilisent le mécanisme des tags natifs et doivent être enregistrés tôt, avant
que l'engine ne finisse d'ajouter les tags natifs. C'est géré en interne, mais cela signifie que ces
tags ne relèvent pas d'un workflow Blueprint — n'essayez pas de les ajouter à l'exécution via
RegisterTag. Et ignorez toute doc plus ancienne mentionnant un Config/Tags/Dialogues/<Name>.ini
par dialogue : aujourd'hui le code produit les deux chemins ci-dessus.
Voir aussi : Dynamic Tags · Dynamic Tags — packaged builds & the .ini fallback