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()lanceFirstDecisionTutorialselon l'état de l'utilisateur;TeacherCreateSimStatelance le tutoriel de création de simulation;TeacherChooseTutorialPopupControlpermet 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 :
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();
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é
- Le ou les textes existent en FR et EN.
- Chaque cible possède un UID
TutorialUIObjectstable. - Chaque étape qui exige un vrai clic possède un
TutorialUIButtoncorrectement 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 vraie 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 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.