Aller au contenu

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ù.

Le widget de debug GPVar après la lecture d'un dialogue

  1. 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ées Done et les flags par reply apparaître d'eux-mêmes — exactement ce que vous auriez créé à la main.
  2. Pour suivre votre propre état, écrivez un GPVar depuis n'importe quel Blueprint ou C++ : RaiseGPFlag / LowerGPFlag pour un booléen, SetGPCounter / GetGPCounter pour un entier, chacun identifié par un GPTagPath (une séquence de gameplay tags, p. ex. Characters.Galeb / Stats.Karma).
  3. 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.

  1. À 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());
    
  2. Au chargement, relisez-les de la même façon, avec un FMemoryReader et ArIsSaveGame = true. Chargez les deux, ou aucun : un historique des conditions sans les GPVars contre lesquels il a été figé donne des dialogues incohérents.

  3. Le chargement remplace tout l'historique et les GPVars. Les GPVars sous le tag racine Transient ne 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.

Les slots de condition Display vs Availability sur un Reply

  1. 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.
  2. 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.
  3. Pour « ce dialogue a-t-il été joué ? », utilisez la ULBRDialogueDoneCondition dé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é.

Une ULBRDialogueDoneCondition configurée sur une réplique

  1. Pour une vérification oui/non, attachez ULBRDialogueDoneCondition : donnez-lui le ou les tags de dialogue ou sous-dialogue, avec le commutateur bCheckAll pour tous vs n'importe lequel quand vous en passez plusieurs.
  2. Pour un seuil (« joué au moins 3 fois »), utilisez ULBRDialogueDoneCountedCondition (dérivée de ULBRCheckGPCounter) pour comparer le nombre de lectures.
  3. 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.

  1. Écrivez la vérification comme une condition — sous-classez ULBRCondition et surchargez Evaluate pour comparer la stat (p. ex. un GPCounter comme Characters.Player / Stats.Lockpick) à la difficulté.
  2. 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. Activez bCacheSuccess / bCacheFailure pour que le résultat soit mis en cache et qu'Evaluate ne 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.
  3. 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 avec CheckHistory (1 = vrai mémorisé, 0 = faux mémorisé, -1 = aucune entrée). Sa map est SaveGame : 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.

Un acteur listener de GPFlag et son événement OnFlagChanged

  1. 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.
  2. 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.

Une propriété LBRGameplayTagFlags dépliée en grille de cases à cocher

  1. 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.
  2. 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.
  3. Sur votre propre propriété, filtrez-la avec Categories (une branche fixe) ou GetCategories (une fonction qui retourne la branche quand l'ensemble valide est contextuel), et ajoutez AutoExpandFlags pour 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.

  1. Dans l'éditeur, les tags sont (re)générés au chargement du projet par le ULBRDialogueTagsSubsystem, qui garde un cache éditeur dans Saved/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.
  2. 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 dans Config/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.
  3. 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.

    Le sous-arbre généré Libretto.Dialogues.<Name> dans le sélecteur de tags

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