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 :
ScenarioSegGridControldans 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
- Ajouter un nouveau bit libre dans
Scenario.eScenarioStepavec1 << n. - Ne jamais changer la valeur d'un ancien bit déjà persisté.
- Tester
IsScenarioStepDone()avant de déclencher la séquence. - À la fin, appeler
SetScenarioStep(). - 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
StudentLoadScenarioStatecharge l'évaluation avant l'entrée dans le scénario.StudentPlayScenarioByYearStateattendeFinishQuestionsavant de passer à l'équipe / activité.StudentPlayScenarioByModuleStatedé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 →
CreateProductTimelineou 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
ScenarioStepsi c'est une progression globale/tutoriel; SetYearSequenceSeen(year)pour les résultats annuels;UserState.ModuleUserState.STUDENT_RESULTSpour 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.