Skip to main content

Scénarios et timelines runtime

But de cette page

Le projet contient trois systèmes proches visuellement mais techniquement distincts :

Système Responsabilité Source de vérité
Scenario Pages de contexte, questions, réponses, segment grid et évaluation. evaluation.scenario + static data + ScenarioPages.
ScenarioStep Flags de progression : tutoriel vu, première action faite, questions terminées, etc. userDetails.scenarioStep.
Timeline Séquences Unity/PlayableDirector : résultats, premier module, lancement produit, etc. Assets .playable + scripts TimelineBase.

Ne pas utiliser un ScenarioStep comme stockage d'une réponse métier et ne pas utiliser une Timeline comme mécanisme de progression principal. Chaque système a son lifecycle propre.

Architecture Scenario

flowchart LR
SD[Static data / ScenarioPages] --> UI[ScenarioPanelControlBase]
UI --> Q[Questions / Segment grid]
Q --> SAVE[SaveScenarioAnswers / SaveEvalSegment]
SAVE --> EVAL[(evaluation.scenario)]
EVAL --> COMPUTE[compute_scenario_score / compute_segment_score]
COMPUTE --> SCORE[evaluation.scores]
SCORE --> CORRECT[ScenarioEval / corrected UI]
UI --> STEP[eFinishQuestions]
STEP --> USER[(userDetails.scenarioStep)]

Modèles principaux

  • Assets/Src/Game/Data/Scenario.cs — réponses booléennes + réponses de segments.
  • ScenarioAnswer.cs — réponse statique attendue par UID.
  • ScenarioEval.cs — scénario + goodScenarioAnswers + goodSegmentAnswers.
  • ScenarioPages.cs — définition des pages affichées.
  • ScenarioPanelControlBase.cs — moteur UI principal.
  • ScenarioSegGridControlBase.cs — base de la grille de segments.
  • EvalCorrectedPopupControl.cs — affichage de la correction.

ScenarioPages : configuration des pages

ScenarioPages est un ScriptableObject. Chaque PagesDefinition contient :

  • headerText — UID/base du titre;
  • segGridUid — segment grid associé si applicable;
  • pages[] — sous-pages de contenu;
  • questionsStartIndex;
  • numQuestions.

Chargement By Year

StaticDataByYearController.Initialize() charge :

Resources.Load<ScenarioPages>($"data_{GameUid}/ScenarioPages");

Le nom et le chemin de l'asset sont donc contractuels.

Chargement By Module

StaticDataByModuleController construit un asset par module :

string moduleUid = moduleByUid.Key.SnakeCaseToPascalCase();
Resources.Load<ScenarioPages>($"data_{GameUid}/{moduleUid}ScenarioPages");

Exemple : un module bank_reconciliation cherchera un asset dont le nom suit la conversion PascalCase du module. Si l'asset n'est pas trouvé, le module n'est simplement pas ajouté à ScenarioPages.

Créer ou modifier un Scenario de A à Z

1. Définir le contenu et les réponses attendues

Les questions utilisent des UIDs du type question_N. Les bonnes réponses viennent de la static data chargée par le jeu. Si une question est ajoutée, vérifier que son UID est cohérent avec l'index utilisé par PagesDefinition.questionsStartIndex/numQuestions.

2. Modifier le ScenarioPages asset

Ajouter la page dans le bon ScenarioPages. Pour By Year, modifier data_<gameUid>/ScenarioPages. Pour By Module, modifier le <ModuleUid>ScenarioPages du module.

L'inspecteur custom ScenarioPagesEditor.cs masque la référence de grille lorsque useSegmentsInScenario est désactivé.

3. Ajouter les textes

Ajouter les textes de page, questions, choix et titres via le pipeline de localisation. Ne pas hardcoder le texte dans ScenarioPanelControlBase.

4. Ajouter un rendu spécifique si nécessaire

By Year possède des contrôles game-specific comme :

  • ScenarioSegGridControl dans MK1/MK2/HR1;
  • ScenarioSegmentsTableMgt1, ScenarioEmployeesTableMgt1, ScenarioFeatureTableMgt1, ScenarioDisciplinaryTableMgt1.

Si le scénario ne peut pas être exprimé avec les composants communs, créer le contrôle dans Assets/GameSpecific/<gameUid>/Src/UI, pas dans Commun.

5. Vérifier la génération runtime des pages

ScenarioPanelControlBase.InitRuntimePages() combine les pagesDefinitions avec les segments du jeu lorsque useSegmentsInScenario est actif. LoadPageDefInContent() instancie ensuite les éléments UI.

6. Sauvegarder les réponses

Lors de la validation, OnConfirmValidateAnswers() construit les nouveaux question_N puis appelle :

flowchart LR
UI[ScenarioPanelControlBase] --> STUD[Student.SaveScenarioAnswers]
STUD --> BE[BackEnd.SaveScenarioAnswers]
BE --> CMD[_command/save_scenario_answers]
CMD --> E[(evaluation.scenario.answers)]

save_scenario_answers.js merge uniquement les réponses reçues dans evaluation.scenario.answers, puis sauvegarde le document evaluation.

7. Sauvegarder les segments

Les réponses de segment passent par SaveEvalSegment → _command/save_eval_segment. Ce script écrit le segment dans evaluation.scenario[segmentUid], sauvegarde, puis envoie au professeur un get_data qui devient update_eval.

8. Calculer le score

Le client appelle les commandes game-specific :

  • <gameUid>_compute_scenario_score;
  • <gameUid>_compute_segment_score.

Ces wrappers délèguent au Eval<Game> correspondant, sauvegardent le score dans evaluation.scores, puis retournent un stub ScenarioEval.

9. Marquer la fin du Scenario

Une fois la séquence terminée, le client appelle :

Game.Student.SetScenarioStep(
    Scenario.eScenarioStep.eFinishQuestions
);

eFinishQuestions doit rester la valeur 1. Le commentaire dans Scenario.cs indique que le serveur suppose explicitement cette valeur.

10. Tester la correction

Tester GetScenarioEvaluation, ScenarioEval et l'UI corrigée. Une nouvelle question n'est pas terminée tant que la vue professeur/correction sait l'afficher.

ScenarioStep : progression, pas évaluation

Scenario.eScenarioStep est un enum de flags. Exemples actuels :

  • eFinishQuestions;
  • eFinishFirstProduct;
  • eStudentAfterFirstResults;
  • eFinishFirstRealLaunch;
  • eStudentFirstModule;
  • eStudentFirstCorrection;
  • eByModuleHomepageTutorial;
  • eStudentFirstFormula;
  • eStudentFirstFormulaActivation.

Ajouter un nouveau ScenarioStep

  1. Ajouter un nouveau bit libre dans Scenario.eScenarioStep avec 1 << n.
  2. Ne jamais changer la valeur d'un ancien bit déjà persisté.
  3. Tester IsScenarioStepDone() avant de déclencher la séquence.
  4. À la fin, appeler SetScenarioStep().
  5. Si la feature peut être rejouée manuellement, décider si UnSetScenarioStep() est nécessaire.

User.SetScenarioStep() OR le bit dans _scenarioFlag puis appelle _command/set_scenario_step, qui persiste le masque complet dans userDetails.scenarioStep.

States liés au Scenario

  • StudentLoadScenarioState charge l'évaluation avant l'entrée dans le scénario.
  • StudentPlayScenarioByYearState attend eFinishQuestions avant de passer à l'équipe / activité.
  • StudentPlayScenarioByModuleState déclenche le welcome By Module puis attend le même flag.

Si un nouveau scénario bloque le flow, debugger d'abord le flag eFinishQuestions et le state courant avant de chercher un problème de navigation UI.

Architecture Timeline

flowchart LR
TRIGGER[Controller / UI / State] --> TL[Timeline subclass]
TL --> BASE[TimelineBase.Play]
BASE --> CAM[Switch cameras + block raycasts]
BASE --> DIR[PlayableDirector.Play]
DIR --> STOP[PlayableDirector.stopped]
STOP --> RESTORE[TimelineBase.Stop]
RESTORE --> CB[onFinish]

TimelineBase : contrat commun

TimelineBase gère :

  • bascule uiCamera → offscreenCamera;
  • activation de scene3dRoot;
  • Canvas rendu par la caméra offscreen;
  • adaptation du scale selon le ratio 16:9 / 4:3;
  • blocage des raycasts UI pendant la séquence;
  • abonnement temporaire à PlayableDirector.stopped;
  • restauration complète dans Stop();
  • callback onFinish.

Timelines existantes

Classe Usage
CreateProductTimeline Séquence générique après création/lancement d'un premier objet métier.
YearResultsTimeline Résultats annuels By Year, graph + résultat positif/négatif.
FirstModuleTimeline Première ouverture de module By Module.
ModuleResultsTimeline Résultats d'un module summative + report de notes.
TimelinesMk1 Extension MK1 pour launch produit.
ENT1 LaunchProductTimeline Launch produit / résultats spécifiques ENT1.

Créer une nouvelle Timeline de A à Z

1. Décider si une Timeline est réellement nécessaire

Utiliser Timeline pour une séquence audiovisuelle qui doit bloquer temporairement l'UI. Pour un guide interactif qui montre des contrôles et attend des clics, utiliser le système Tutoriels.

2. Créer l'asset .playable

Créer/configurer l'asset Unity Timeline dans le dossier Resources approprié, puis créer les tracks/animations nécessaires.

3. Créer une classe héritant TimelineBase

public class MyTimeline : TimelineBase
{
    public static MyTimeline Instance;

    [SerializeField] private PlayableDirector director;

    private void Awake()
    {
        Instance = this;
    }

    public void PlaySequence(string message, Action onFinish)
    {
        character3dText.text = message;
        Play(onFinish, director);
    }
}

4. Brancher toutes les références Unity

Le prefab/scene doit fournir au minimum les caméras, scene3dRoot, canvas group, screen material/transform, texte 3D et le PlayableDirector.

5. Choisir le déclencheur

Exemples réels :

  • DecisionsControlByModule → FirstModuleTimeline;
  • DecisionsControlByModule → ModuleResultsTimeline;
  • StudentState / TeamControllers → YearResultsTimeline;
  • contrôles de création/lancement produit → CreateProductTimeline ou timeline game-specific.

6. Décider comment empêcher le replay

Une Timeline n'a pas elle-même de persistance. Utiliser selon le cas :

  • un ScenarioStep si c'est une progression globale/tutoriel;
  • SetYearSequenceSeen(year) pour les résultats annuels;
  • UserState.ModuleUserState.STUDENT_RESULTS pour le résultat d'un module;
  • une condition métier comme “premier produit seulement”.

7. Toujours terminer le flow dans onFinish

La logique qui ouvre l'écran suivant, change le state ou marque la progression doit être appelée après la fin de la Timeline. Ne pas supposer que Play() est synchrone.

Cas spécial ModuleResultsTimeline

ModuleResultsTimeline peut être déclenchée avant que les résultats soient arrivés. Elle s'abonne alors à Game.CurrentTeamByModule.onNewTeamModuleResults, filtre sur le module/type, puis joue la séquence lorsque les données existent.

Si une nouvelle Timeline attend un RTT ou une donnée backend, reprendre ce pattern : abonnement temporaire, filtre strict, désabonnement dès réception.

Debug Scenario

Symptôme Vérifier
Page absente Nom/path du ScenarioPages, pagesDefinitions, import Resources.
Question affichée mais réponse non persistée question_N → SaveScenarioAnswers → evaluation.scenario.answers.
Segment non mis à jour chez le prof save_eval_segment → message get_data/update_eval.
Score toujours faux Static good answers + Eval<Game>.computeScenarioScoreAndSave().
Étudiant bloqué après les questions userDetails.scenarioStep contient-il eFinishQuestions ?
Correction vide get_scenario_evaluation, ScenarioEval.Deserialize(), corrected popup.

Debug Timeline

Symptôme Vérifier
Écran noir après la séquence TimelineBase.Stop() est-il exécuté ? Le director a-t-il émis stopped ?
UI non cliquable après UICanvasGroup.blocksRaycasts doit revenir à true.
Timeline rejouée à chaque ouverture Le flag de persistance approprié n'est pas sauvegardé.
Callback exécuté plusieurs fois Vérifier l'abonnement/désabonnement à director.stopped et aux events résultats.
Résultats incorrects dans l'animation Vérifier l'année et les RankingsData passés, pas l'animation elle-même.
Layout déformé Références scale_16_9/scale_4_3 et screen transform.

Checklist avant merge

  • Scenario : asset Resources au bon nom.
  • Scenario : textes FR/EN présents.
  • Scenario : réponse sauvegardée puis relue après relog.
  • Scenario : score game-specific correct.
  • Scenario : correction professeur/étudiant correcte.
  • ScenarioStep : aucun bit historique renuméroté.
  • Timeline : prefab/director/caméras référencés.
  • Timeline : UI restaurée à la fin.
  • Timeline : condition anti-replay persistée.
  • Timeline : onFinish continue le flow.
  • WebGL testé pour les deux systèmes.