Skip to main content

Tutoriels — création de A à Z et architecture

But de cette page

Cette page explique comment cré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 tutoriel avant de coder

Besoin Solution
Afficher un message + éventuellement surligner une zone, puis laisser l'utilisateur cliquer pour fermer 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 classe qui hérite de MenuTutorialSequence.
Tutoriel commun à tous les jeux Assets/Src/Game/Tutorial/.
Tutoriel propre au framework By Module Assets/Src/UI/SimByModule/Tutorial/.
Tutoriel propre à MK1, HR1, ENT1, etc. Assets/GameSpecific/<gameUid>/Src/Tutorial/ ou dossier UI du jeu si c'est déjà la convention de ce jeu.
Tutoriel joué une seule fois par utilisateur Le déclenchement doit tester un Scenario.eScenarioStep, puis appeler SetScenarioStep() lorsque le tutoriel est réellement complé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'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'écrire le contenu.

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

Pour chaque élément à surligner ou suivre, ouvrir le prefab correspondant dans Unity et ajouter TutorialUIObject au GameObject possédant le RectTransform. Dans l'Inspector, remplir le champ uiObejctName avec un UID stable, par exemple :

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 trouver/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 localisation existant. Ajouter les UID FR/EN dans la Google Sheet appropriée puis utiliser le pipeline documenté dans Textes, localisation et mise à jour.

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 résoudre le texte.

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

Créer une classe qui hérite de MenuTutorialSequence 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 RectTransform 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 une flèche 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é

  1. Le ou les textes existent en FR et EN.
  2. Chaque cible possède un UID TutorialUIObject stable.
  3. Chaque étape qui exige un vrai clic possède un TutorialUIButton correctement branché au Button/BasicButton/Toggle.
  4. Les cibles existent réellement au moment du lookup; sinon le code attend leur apparition.
  5. La bonne caméra est utilisée pour chaque popup/panel.
  6. Le trigger ne peut pas relancer le tutoriel en boucle.
  7. Le ScenarioStep/UserState n'est marqué terminé qu'après la vraie réussite.
  8. Skip/fermeture d'écran ne laisse ni highlight ni blocker.
  9. Le tutoriel fonctionne après logout/login.
  10. 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 considéré terminé trop tôt SetScenarioStep() 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 guider l'utilisateur. L'action réelle reste dans les contrôles/controllers existants; le tutoriel attend simplement que cette action se produise.