# 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

<table id="bkmrk-besoin-solution-affi"><thead><tr><th>Besoin</th><th>Solution</th></tr></thead><tbody><tr><td>Afficher un message + éventuellement surligner une zone, puis laisser l'utilisateur cliquer pour fermer</td><td>`GenericTutorial`. Ne pas créer une nouvelle classe.</td></tr><tr><td>Forcer l'utilisateur à cliquer sur un vrai bouton/toggle et enchaîner plusieurs étapes</td><td>Créer une classe qui hérite de `MenuTutorialSequence`.</td></tr><tr><td>Tutoriel commun à tous les jeux</td><td>`Assets/Src/Game/Tutorial/`.</td></tr><tr><td>Tutoriel propre au framework By Module</td><td>`Assets/Src/UI/SimByModule/Tutorial/`.</td></tr><tr><td>Tutoriel propre à MK1, HR1, ENT1, etc.</td><td>`Assets/GameSpecific/<gameUid>/Src/Tutorial/` ou dossier UI du jeu si c'est déjà la convention de ce jeu.</td></tr><tr><td>Tutoriel joué une seule fois par utilisateur</td><td>Le déclenchement doit tester un `Scenario.eScenarioStep`, puis appeler `SetScenarioStep()` lorsque le tutoriel est réellement complété.</td></tr><tr><td>État propre à une simulation plutôt qu'au compte utilisateur global</td><td>Utiliser le système `UserState` plutôt qu'ajouter automatiquement un ScenarioStep.</td></tr></tbody></table>

## Architecture réelle

```mermaid
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

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

## 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 :

```csharp
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 :

```csharp
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 :

```csharp
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 :

```csharp
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`.

```csharp
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é**.

<table id="bkmrk-comportement-voulu-%C3%80"><thead><tr><th>Comportement voulu</th><th>À utiliser</th></tr></thead><tbody><tr><td>“Lis ce message puis clique n'importe où pour continuer”</td><td>`WaitForClick()` + `_isClicked`</td></tr><tr><td>“Clique exactement sur ce bouton pour continuer”</td><td>`TutorialUIButton.clicked`</td></tr><tr><td>“Attends quelques secondes”</td><td>`WaitForTime()` + `TimeIsReached`</td></tr></tbody></table>

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

```csharp
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 :

```csharp
new MyFeatureTutorial().Start();
```

Pour un tutoriel one-shot :

```csharp
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 :

```csharp
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 :

```csharp
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 :

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

Avec popup :

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

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

```csharp
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

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

## 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

<table id="bkmrk-sympt%C3%B4me-cause-proba"><thead><tr><th>Symptôme</th><th>Cause probable</th></tr></thead><tbody><tr><td>`GetUiObject()` retourne null</td><td>Prefab/panel inactif, UID erroné, ou composant non enregistré.</td></tr><tr><td>Le tutoriel attend éternellement un clic</td><td>`TutorialUIButton` absent ou sa référence `button/basicButton/toggle` n'est pas assignée.</td></tr><tr><td>Highlight/flèche décalé</td><td>Mauvaise caméra, souvent `UICamera` utilisée sur une popup.</td></tr><tr><td>Le clic sur la cible ne fait rien</td><td>Le code utilise `WaitForClick()` alors qu'il devrait attendre `TutorialUIButton.clicked`.</td></tr><tr><td>Le tutoriel rejoue à chaque login</td><td>ScenarioStep/UserState jamais sauvegardé.</td></tr><tr><td>Le tutoriel est considéré terminé trop tôt</td><td>`SetScenarioStep()` est appelé au démarrage au lieu de la fin.</td></tr><tr><td>La mauvaise cible est surlignée</td><td>Plusieurs objets actifs utilisent le même `uiObejctName`.</td></tr><tr><td>Un nouveau tutoriel coupe l'ancien</td><td>Comportement normal : `MenuTutorialSequence.Start()` arrête le tutoriel courant.</td></tr></tbody></table>

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