Skip to main content

Tutoriels — création de A à Z et architecture

ScénariosBut de cette page

LeCette projetpage possèdeexplique descomment structurescréer, déclencher, persister, tester et déboguer un tutoriel Unity dans UnikSim. Elle ne décrit pas Timeline : ce sont deux systèmes séparés dans le code.

Choisir le bon type de scénariotutoriel communesavant dansde coder

Besoin Solution Afficher un message + éventuellement surligner une zone, puis laisser l'utilisateur cliquer pour fermer Assets/Src/Game/Data/Scenario*GenericTutorial. Ne pas créer une nouvelle classe. Forcer l'utilisateur à cliquer sur un vrai bouton/toggle et enchaîner plusieurs étapes Créer une UIclasse communequi dans Assets/Src/UI/Scenario*. Chaque jeu peut ensuite fournir ses pages, réponses et prefabs spécifiques.

ScenarioPages

Les contrôleurshérite de staticMenuTutorialSequence.

data chargentdesTutoriel ScenarioPagescommun depuisà Resources.tous Enles Byjeux Module, StaticDataByModuleController.Initialize() tente notamment de charger data_<GameUid>/<ModuleUid>ScenarioPages pour chaque module.

Tutoriels

Assets/Src/Game/Tutorial/. contient lesbriquesTutoriel communes.propre Lesau implémentationsframework spécifiquesBy seModule trouventAssets/Src/UI/SimByModule/Tutorial/. ensuite dans Tutoriel propre à MK1, HR1, ENT1, etc. Assets/GameSpecific/<gameUid>/Src/Tutorial/ ou dansdossier UI du jeu si c'est déjà la familleconvention Byde Module.ce Unjeu. Tutoriel joué une seule fois par utilisateur Le déclenchement doit tester un Scenario.eScenarioStep, puis appeler SetScenarioStep() lorsque le tutoriel peutest doncréellement dépendrecomplété. État propre à une simulation plutôt qu'au compte utilisateur global Utiliser le système UserState plutôt qu'ajouter automatiquement un ScenarioStep.

Architecture réelle

flowchart TD
TRIGGER[GameState / UI / événement] --> SEQ[MenuTutorialSequence]
SEQ --> CHARACTER[TutorialCharacter]
SEQ --> LOOKUP[TutorialUIObject.GetUiObject]
LOOKUP --> TARGET[RectTransform actif]
SEQ --> HIGHLIGHT[TutorialSquareHighlight]
SEQ --> ARROW[TutorialArrow]
SEQ --> CLICK{Type d'attente}
CLICK -->|clic overlay| CATCHER[TutorialClickCatcherControl]
CLICK -->|vrai bouton| BUTTON[TutorialUIButton.clicked]
SEQ --> DONE[SetScenarioStep / SetUserState]
DONE --> STOP[Status.Stop]
STOP --> CLEAN[cleanup automatique]

Composants à connaître

Classe Responsabilité TutorialControl Singleton contenant character, highlight, blocker, click catcher, arrow et bouton Skip. MenuTutorialSequence Classe de base des tutoriels complexes. Gère Start/Stop, nettoyage et helpers. GenericTutorial Tutoriel simple “message + highlight éventuel + clic pour continuer”. TutorialUIObject Enregistre un RectTransform actif sous un UID afin qu'un tutoriel puisse le retrouver. TutorialUIButton Étend TutorialUIObject et détecte les clics d'un Button, BasicButton ou Toggle. TutorialSquareHighlight Cadre/masque visuel autour d'une cible. TutorialArrow Place la flèche relativement à la cible. TutorialClickCatcherControl Overlay cliquable utilisé par WaitForClick(). TutorialInputBlockerControl Permet de bloquer une partie ou la totalité des inputs.

Créer un tutoriel de zéro — procédure complète

1. Décider ce qui déclenche le tutoriel

Commencer par le trigger, pas par la classe du tutoriel. Exemples réels :

    StudentState.TransitionIn() lance FirstDecisionTutorial selon l'état de l'UI,utilisateur; TeacherCreateSimState lance le tutoriel de création de simulation; TeacherChooseTutorialPopupControl permet de rejouer manuellement certains tutoriels; une validation UI peut lancer directement un GenericTutorial.

    Déterminer également si le tutoriel doit être joué une seule fois. Si oui, choisir son mécanisme de persistance avant d'unécrire prefab,le d'uncontenu.

    message

    2. Rendre les éléments UI trouvables par le tutoriel

    Pour chaque élément à surligner ou d'unsuivre, state.

    Timelines

    Assets/Src/Game/Timeline/ contientouvrir le socle,prefab tandis que des jeux fournissent leurs propres assets Timelinecorrespondant dans Unity et ajouter GameSpecific/<gameUid>/Resources/TimelineTutorialUIObject au GameObject possédant le RectTransform. MK1/MK2Dans utilisentl'Inspector, remplir le champ uiObejctName avec un UID stable, par exemple des:

    timelines
    create_product_button
    calculator_button
    cash_label
    my_new_feature_button

    Le tutoriel pourra ensuite récupérer la cible avec :

    RectTransform target = TutorialUIObject.GetUiObject("my_new_feature_button");

    Important : TutorialUIObject s'enregistre dans le dictionnaire statique uniquement dans OnEnable(). Si le GameObject/prefab/panel n'est pas actif, GetUiObject() retourne null. Il faut alors ouvrir le panel avant le lookup ou attendre jusqu'à ce que la cible existe.

    Si plusieurs variantes d'UI peuvent représenter la même étape, utiliser :

    string[] candidates = { "button_practice", "button_summative", "button_consultation" };
    RectTransform target = TutorialUIObject.GetFirstUiObject(candidates);

    Éviter les UID dupliqués sans raison : le dictionnaire garde une liste mais GetUiObject() retourne le premier élément actif. Deux objets actifs portant le même UID peuvent donc viser la mauvaise cible.

    3. Si l'utilisateur doit cliquer sur la vraie cible, utiliser TutorialUIButton

    Un simple TutorialUIObject permet de création/lancementtrouver/surligner l'objet, mais il ne sait pas si l'utilisateur l'a activé. Pour une étape qui doit attendre un vrai clic, utiliser TutorialUIButton.

    Dans l'Inspector, il faut assigner explicitement l'un des champs correspondant au contrôle réel :

      button pour un UnityEngine.UI.Button; basicButton pour un BasicButton; toggle pour un Toggle.

      La classe ne fait pas automatiquement un GetComponent<Button>(). Si aucune référence n'est assignée, clicked ne passera jamais à true.

      Pattern réel :

      RectTransform target = TutorialUIObject.GetUiObject("my_new_feature_button");
      TutorialUIButton tutoButton = target.GetComponent<TutorialUIButton>();
      
      while (!tutoButton.clicked)
      {
          ShowSquareHighlight(target);
          ShowArrow(target);
          yield return Status.Running;
      }
      
      HideArrow();
      RemoveSquareHighlight();

      TutorialUIButton.clicked s'auto-reset après avoir été lu comme true. Il faut donc l'utiliser comme un signal ponctuel, typiquement dans un while (!tutoButton.clicked).

      4. Ajouter les textes

      Les textes doivent normalement passer par le système de produitlocalisation existant. Ajouter les UID FR/EN dans la Google Sheet appropriée puis utiliser le pipeline documenté dans Textes, localisation et demise résultats.à jour.

      Checklist

      Dans le code :

      ShowCharacter(
          Game.Texts.GetText("my_tutorial_step_1"),
          TutorialCharacter.eCharacterPos.eTop,
          true
      );

      GenericTutorial accepte soit un UID existant, soit une chaîne déjà résolue. Il teste Game.Texts.HasText() avant de modificationrésoudre

      1. Identifierle texte.

        5. Créer la classe pour un tutoriel multi-étapes

        Créer une classe qui hérite de scénario/tutoriel.

      2. IdentifierMenuTutorialSequence et surcharger Content(). Le moteur est une séquence coroutine : tant qu'une étape attend quelque chose, retourner Status.Running; à la fin, retourner Status.Stop.
        using System.Collections.Generic;
        using UnityEngine;
        
        namespace UnikSim
        {
            public class MyFeatureTutorial : MenuTutorialSequence
            {
                public override IEnumerator<Status> Content()
                {
                    ShowCharacter(
                        Game.Texts.GetText("my_feature_tuto_1"),
                        TutorialCharacter.eCharacterPos.eTop,
                        true
                    );
        
                    WaitForClick(0.1f);
                    while (!_isClicked)
                    {
                        yield return Status.Running;
                    }
        
                    RectTransform target = null;
                    while (target == null)
                    {
                        target = TutorialUIObject.GetUiObject("my_feature_button");
                        yield return Status.Running;
                    }
        
                    TutorialUIButton tutoButton = target.GetComponent<TutorialUIButton>();
        
                    ShowCharacter(
                        Game.Texts.GetText("my_feature_tuto_2"),
                        TutorialCharacter.eCharacterPos.eTop,
                        false
                    );
        
                    while (!tutoButton.clicked)
                    {
                        ShowSquareHighlight(target);
                        ShowArrow(target);
                        yield return Status.Running;
                    }
        
                    HideArrow();
                    RemoveSquareHighlight();
                    RemoveCharacter();
        
                    Game.User.SetScenarioStep(Scenario.eScenarioStep.eMyFeatureTutorial);
        
                    yield return Status.Stop;
                }
            }
        }

        6. Comprendre WaitForClick : ce n'est PAS un clic sur la cible

        WaitForClick() active TutorialClickCatcherControl, un overlay global. L'utilisateur clique sur l'overlay pour continuer. Ce helper est donc parfait pour une étape informative, mais pas pour exiger qu'un bouton métier soit réellement pressé.

        Comportement voulu À utiliser “Lis ce message puis clique n'importe où pour continuer” WaitForClick() + _isClicked “Clique exactement sur ce bouton pour continuer” TutorialUIButton.clicked “Attends quelques secondes” WaitForTime() + TimeIsReached

        7. Choisir la bonne caméra

        Les helpers utilisent une caméra pour convertir le prefabRectTransform en position écran.

        • UI normale : Game.Instance.UICamera — utilisé par défaut;
        popup : Game.Instance.PopupCamera; certains cas game-specific peuvent fournir explicitement une autre caméra.
        ShowSquareHighlight(target, Game.Instance.PopupCamera);
        ShowArrow(target, Game.Instance.PopupCamera);

        Un highlight décalé ou assetune ScriptableObject/Timelineflèche associé.

        au mauvais endroit est souvent simplement une mauvaise caméra.

        8. Déclencher le tutoriel

        Le lancement standard est :

        new MyFeatureTutorial().Start();

        Pour un tutoriel one-shot :

        if (!Game.User.IsScenarioStepDone(Scenario.eScenarioStep.eMyFeatureTutorial))
        {
            new MyFeatureTutorial().Start();
        }

        Le meilleur endroit est généralement l'endroit où l'état nécessaire vient d'être atteint : un GameState, l'ouverture d'un panel, la création d'une donnée ou la réception d'un résultat. Ne pas lancer un tutoriel dans Update() sans garde : il serait recréé continuellement.

        9. Ajouter un ScenarioStep si le tutoriel doit être mémorisé

        Scenario.eScenarioStep est un enum [Flags]. Chaque valeur représente un bit. Les valeurs existantes utilisent 1 << n.

        Pour ajouter une nouvelle étape persistante :

        eMyFeatureTutorial = 1 << 18

        Prendre le prochain bit libre et ne jamais réutiliser un bit existant. eFinishQuestions = 1 doit rester égal à 1 : le commentaire du code indique que le serveur dépend de cette valeur.

        À la fin réelle du tutoriel :

        Game.User.SetScenarioStep(Scenario.eScenarioStep.eMyFeatureTutorial);

        SetScenarioStep() ajoute le bit au masque local puis, par défaut, appelle Game.Backend.SetScenarioStep() pour sauvegarder le masque côté backend.

        Si le statut doit être propre à une simulation plutôt qu'au compte utilisateur global, examiner User.SetUserState(simUid, stateUid) / IsUserStateDone() au lieu d'utiliser un nouveau ScenarioStep.

        10. Cas simple : ne PAS créer une nouvelle classe

        Pour un message de validation ou une aide one-shot sans workflow :

        new GenericTutorial(
            "not_enough_budget_body",
            "games_budget",
            TutorialCharacter.eCharacterPos.eDown
        ).Start();

        Avec popup :

        new GenericTutorial(
            "loan_impossible",
            "loan_max",
            TutorialCharacter.eCharacterPos.eTop,
            Game.Instance.PopupCamera
        ).Start();

        Pour chaîner les options de GenericTutorial, utiliser StartGeneric() :

        new GenericTutorial(
            "not_enough_budget_body",
            "games_budget",
            TutorialCharacter.eCharacterPos.eDown
        ).StartGeneric().ShowFlashing();

        Ce que MenuTutorialSequence fait automatiquement

        Start() arrête le tutoriel actuellement actif avant de démarrer le nouveau. Stop() nettoie character, square highlight, arrow, input blocker, click catcher et bouton Skip.

        Le bouton Skip est activé par défaut pour le professeur via Game.User.IsTeacher. Certains tutoriels “force watch”, comme la création initiale de simulation, le désactivent explicitement.

        Exemples du code à copier selon le besoin

        Besoin Exemple existant Premier tutoriel étudiant simple Assets/Src/Game/Tutorial/Student/FirstDecisionTutorial.cs Tutoriel By Module avec plusieurs cibles alternatives FirstDecisionByModuleTutorial.cs Long tutoriel professeur composé de sous-séquences TeacherCreateSimTutorial.cs Extension spécifique By Module TeacherCreateSimTutorialByModule.cs Tutoriel game-specific complexe GameSpecific/mk1/Src/Tutorial/CreateFirstProductTutorial.cs Tutoriel Accounting complexe GameSpecific/acc1/src/AccGeneralTutorials.cs et GenericAcc1Tutorial.cs Aide/validation très simple Rechercher new GenericTutorial dans les contrôles UI.

        Checklist avant de considérer le tutoriel terminé

          VérifierLe ou les textes utilisés.existent en FR et EN. VérifierChaque sicible possède un UID TutorialUIObject stable. Chaque étape qui exige un vrai clic possède un TutorialUIButton correctement branché au Button/BasicButton/Toggle. Les cibles existent réellement au moment du lookup; sinon le code attend leur apparition. La bonne caméra est utilisée pour chaque popup/panel. Le trigger ne peut pas relancer le tutoriel en boucle. Le ScenarioStep/UserState n'est marqué terminé qu'après la progressionvraie réussite. Skip/fermeture d'écran ne laisse ni highlight ni blocker. Le tutoriel fonctionne après logout/login. Le tutoriel fonctionne dans le build WebGL, pas uniquement dans l'Editor.

          Debug — symptômes fréquents

          Symptôme Cause probable GetUiObject() retourne null Prefab/panel inactif, UID erroné, ou composant non enregistré. Le tutoriel attend éternellement un clic TutorialUIButton absent ou sa référence button/basicButton/toggle n'est pas assignée. Highlight/flèche décalé Mauvaise caméra, souvent UICamera utilisée sur une popup. Le clic sur la cible ne fait rien Le code utilise WaitForClick() alors qu'il devrait attendre TutorialUIButton.clicked. Le tutoriel rejoue à chaque login ScenarioStep/UserState jamais sauvegardé. Le tutoriel est sauvegardéeconsidéré serveur.Testerterminé retourtrop arrière,tôt relogSetScenarioStep() est appelé au démarrage au lieu de la fin. La mauvaise cible est surlignée Plusieurs objets actifs utilisent le même uiObejctName. Un nouveau tutoriel coupe l'ancien Comportement normal : MenuTutorialSequence.Start() arrête le tutoriel courant.

          Règle de conception

          Un tutoriel ne devrait pas dupliquer la logique métier. Il doit observer l'UI et repriseguider del'utilisateur. simulation.

          L'action réelle reste dans les contrôles/controllers existants; le tutoriel attend simplement que cette action se produise.