Game Commun Vue d’ensemble du Game Rôle de cette page Cette page est la carte d'entrée du projet Game. La documentation suit les frontières réelles du code : Commun, By Module et By Year. flowchart TD GAME[Unity Game] --> COM[Commun] GAME --> BM[By Module] GAME --> BY[By Year] COM --> CORE[Bootstrap / Controllers / UI] COM --> BC[BrainCloud / Documents / RTT] COM --> PIPE[Static Data / Textes / Build] BM --> ACC[Accounting: acc_base + acc1 + acc2] BY --> GAMES[ENT1 / HR1 / MGT1 / MK1 / MK2] Comment choisir le bon chapitre Chapitre Responsabilité Exemples Commun Tout ce qui est partagé entre familles ou structure le runtime général. Game.cs, Factory, controllers, state machine, backend, Custom Entities, RTT, textes, build. By Module Framework de modules/pages et domaine Accounting. teamsModules, Module Type Data, bills, inventory, answer key, grading, acc1/acc2. By Year Framework d'années/décisions et implémentations des jeux annuels. cycle annuel, jobs, génération résultats, bots, ENT1/HR1/MGT1/MK1/MK2. Architecture globale sequenceDiagram actor User as Utilisateur participant Game as Unity Game participant Factory as Factory participant Ctrl as Controllers participant BE as BackEndBrainCloud participant CS as Cloud Scripts participant DB as Custom Entities participant RTT as BrainCloud RTT User->>Game: lance / interagit Game->>Factory: crée classes selon GameUid + SimType Factory-->>Ctrl: modèles/controllers concrets Ctrl->>BE: opération métier BE->>CS: RunScript CS->>DB: read / update / save CS->>RTT: message si synchronisation nécessaire CS-->>BE: response RTT-->>BE: changement distant BE-->>Ctrl: MessageData / ServerMessage Ctrl-->>Game: modèle + UI mis à jour Points d'entrée Unity Assets/Src/Game/Game.cs — bootstrap, environnement, services globaux, GameUid et SimType. Assets/Src/Game/Factory.cs — composition des classes concrètes. Assets/Src/Game/GameStates/ — login, chargement et navigation professeur/étudiant. Assets/Src/Game/Controllers/ — modèles runtime, caches et listeners. Assets/Src/Game/Data/ — données sérialisées communes, By Module et By Year. Assets/Src/Game/BackEnd/ — contrat backend et implémentation BrainCloud. Assets/Src/UI/ — UI commune et spécialisations. Assets/GameSpecific/ — code, Resources et prefabs propres à chaque jeu. Points d'entrée Node / BrainCloud backend/braincloud/server_scripts/_command/ — Cloud Scripts communs appelables. backend/braincloud/server_scripts/_utils/ — documents, messages, eval, rankings et logique partagée. .../_utils/by_module/ — moteurs By Module. .../acc_base, acc1, acc2 — Accounting. .../ent1, hr1, mgt1, mk1, mk2 — moteurs By Year game-specific. node.js/exports/ — Google Sheets → JSON → BrainCloud et pipeline des textes. backend/braincloud/debugging/ — exécution locale des Server Scripts. backend/braincloud/tests/ — tests automatisés. Méthode pour tracer une feature Identifier le contrôle UI et le controller Unity. Trouver la méthode correspondante dans BackEndBase/ BackEndBrainCloud. Identifier le Cloud Script commun ou game-specific réellement appelé. Suivre ses bridge.include(). Identifier chaque document lu ou sauvegardé. Identifier le message RTT envoyé. Retrouver le listener Unity qui consomme ce message. Si la donnée est générée/configurée, remonter aussi au JSON/Google Sheet source. Ordre de lecture conseillé Pour prendre le projet en main : Bootstrap → Machine d'état → Controllers → Simulations/activités/équipes → Backend Unity ↔ BrainCloud → Documents/RTT, puis entrer dans By Module ou By Year. Carte fonctionnelle des jeux Pour comprendre rapidement le rôle de chaque simulation avant d’entrer dans le code, voir Les jeux UnikSim — rôle, contenu et navigation de la documentation. Cette page résume ENT1, HR1, MGT1, MK1, MK2, ACC1 et ACC2 et indique quelles pages techniques ouvrir ensuite. Bootstrap et composition runtime Responsabilité Le bootstrap commun décide où le client se connecte, quel jeu est chargé, quelle famille de simulation est utilisée et quelles classes concrètes sont créées. Le point central est Assets/Src/Game/Game.cs. flowchart TD AWAKE[Game.Awake] --> ENV[SetConnectionType] AWAKE --> SERVICES[Services globaux] ENV --> BC[BrainCloudWrapper] LOGIN[Login] --> UID[SetGameUid] UID --> SIMTYPE[CurrentSimType] UID --> TEXTS[LoadGameSpecificTexts] UID --> FACTORY[Factory.CreateStaticDataController] FACTORY --> BM[By Module] FACTORY --> BY[By Year] Game.Awake() Awake() initialise le singleton Game.Instance, PoolsManager et les objets backend. En Editor, SetConnectionType(GameConnectionType.eDev) est utilisé par défaut. Les wrappers GameSparks/BrainCloud sont désactivés avant la sélection du backend puis le wrapper nécessaire est activé/configuré. Environnements BrainCloud SetConnectionType() sélectionne l'application BrainCloud correspondant à l'environnement courant (Dev, Dev2, Demo ou Live), initialise BrainCloudWrapper, active son GameObject et configure notamment la profondeur JSON maximale. Les identifiants/secrets d'environnement sont du code opérationnel sensible : la documentation décrit leur emplacement et le flux, mais ne doit pas recopier les secrets. InitBackendState → AuthState sequenceDiagram participant S as InitBackendState participant G as Game participant B as Backend participant A as AuthState S->>G: SetBackend(backEnd) S->>G: GlobalTime.GetTime() S-->>A: username + pass + gameUid A->>B: Login(...) B-->>A: profileId + createdAt + displayName A-->>G: LoadUserState SetGameUid() Après chargement de UserDetails, Game.SetGameUid(gameUid, skinName) : convertit le string en SharedConstants.GameUid.eGameUid; renseigne CurrentGameUidEnum et CurrentGameUid; détermine CurrentSimType avec SharedConstants.SimType.GetSimTypeFromGameUid(); charge les textes game-specific; crée le controller de static data via Factory.CreateStaticDataController(); détermine le type d'école et poursuit l'initialisation. Factory Assets/Src/Game/Factory.cs est le point de composition. Il sélectionne les classes concrètes selon le GameUid, au lieu de disperser des if (gameUid) dans le projet. GameUid Famille StaticDataController ent1 By Year StaticDataControllerEnt1 mk1 By Year StaticDataControllerMk1 hr1 By Year StaticDataControllerHr1 mk2 By Year StaticDataControllerMk2 mgt1 By Year StaticDataControllerMgt1 acc1 By Module StaticDataControllerByModule / spécialisation Acc1 acc2 By Module StaticDataControllerByModule / spécialisation Acc2 Factory : autres objets construits La Factory ne sert pas seulement à la static data. Les controllers communs appellent aussi des méthodes comme CreateSimulationBase(), CreateActivityBase(), CreateTeamBase(), CreateTeam(), CreateStudentStub(), CreateTeamStub() et CreateResults(). Le GameUid/SimType choisit ainsi le modèle concret à tous les niveaux. LoadUserState LoadUserState charge UserDetails, crée Teacher ou Student, applique la langue, appelle SetGameUid(), démarre le heartbeat hors Editor et, en WebGL, tente de mettre à jour dynamiquement textes et écoles. Version client Le runtime charge Assets/Resources/data/version.txt et sub_version.txt. Le workflow de build et l'absence d'incrément automatique sont documentés dans Build Unity, WebGL et versioning client. Ajouter un nouveau GameUid Ajouter le GameUid dans SharedConstants. Mapper le GameUid vers le bon SimType. Ajouter les classes concrètes nécessaires dans Factory. Créer Assets/GameSpecific/ et ses Resources. Ajouter les static data/textes dans les pipelines Node. Ajouter les Server Scripts game-specific et les factories serveur si la logique diffère. Tester le flow complet : login → SetGameUid → static data → simulation → commande serveur. Diagnostic Si le mauvais jeu, la mauvaise UI ou le mauvais modèle est chargé, vérifier dans l'ordre : UserDetails.gameUid → SetGameUid() → CurrentSimType → Factory → StaticData*.Initialize(). Machine d’état client — login, chargement et navigation Architecture Le démarrage et la navigation principale utilisent des GameState. Chaque état effectue une opération asynchrone puis retourne Result.TransitionTo(...) lorsque la condition de transition est remplie. flowchart TD INIT[InitBackendState] --> AUTH[AuthState] AUTH --> USER[LoadUserState] USER -->|prof sans école| TS[TeacherSetSchoolState] USER -->|prof| TLS[TeacherLoadSimStubsState] TLS --> TLOAD[TeacherLoadSimState] TLOAD --> TSCENE[TeacherLoadSceneState] USER -->|étudiant sans école| CHOOSE[StudentChooseTeacherAndSimState] USER -->|étudiant| SLOAD[StudentLoadSimState] SLOAD --> WAIT[StudentWaitForSimStartedState] SLOAD --> SCEN[Scenario State] SLOAD --> TEAM[StudentChooseTeamState] SLOAD --> STUD[StudentState] InitBackendState Sélectionne l'implémentation backend, récupère l'heure globale, dérive le username si nécessaire, puis passe à AuthState. AuthState Appelle Game.Backend.Login(). En cas de succès, passe à LoadUserState. En Editor, un échec peut revenir vers EditorLoginState. LoadUserState Charge UserDetails, crée Teacher ou Student, applique langue et GameUid, initialise heartbeat, met à jour textes/écoles sur WebGL puis choisit le prochain état. StudentLoadSimState Après chargement de la simulation, l'état choisit entre attente de démarrage, scénario By Year, scénario By Module, activité non commencée, choix d'équipe ou entrée directe dans StudentState. Teacher flow Le professeur charge d'abord les simulation stubs. S'il en possède, TeacherLoadSimState charge la simulation sélectionnée; sinon il va directement vers la scène de gestion. Debug Un client “bloqué” sur un écran n'est pas nécessairement un bug UI. Identifier d'abord le GameState courant et la condition de son Update() qui n'est jamais satisfaite. Créer un nouvel état de A à Z Un GameState doit représenter une étape de navigation ou de chargement, pas une logique métier arbitraire. Avant d'en créer un, vérifier si le besoin est réellement une transition d'état ou simplement une méthode d'un controller/UI. 1. Choisir la classe de base Les états sont sous Assets/Src/Game/GameStates/. Reprendre le state le plus proche du contexte : étudiant, professeur, login, simulation, etc. Le state doit retourner des Result de la machine commune. 2. Mettre l'initialisation dans TransitionIn() TransitionIn() sert à lancer l'opération asynchrone une seule fois : backend request, ouverture d'un panel, initialisation d'un flag. Éviter de déclencher la même requête dans Update(). public class MyLoadState : GameState { private bool _loaded; public override void TransitionIn() { _loaded = false; Game.Backend.SomeRequest(() => _loaded = true); } public override Result Update() { if (_loaded) return Result.TransitionTo(new NextState()); return Result.Continue; } } 3. Garder Update() déterministe Update() doit essentiellement tester les conditions de sortie. Un state qui envoie une requête chaque frame, crée un popup continuellement ou démarre plusieurs tutoriels est presque toujours mal structuré. 4. Brancher la transition depuis le state précédent La nouvelle classe n'est utile que si un state existant peut la retourner. Rechercher Result.TransitionTo(...) dans le flow concerné et brancher la nouvelle condition au bon endroit. 5. Nettoyer dans TransitionOut()/Dispose si nécessaire Si le state attache des callbacks temporaires, listeners UI ou abonnements globaux, ils doivent être retirés lorsqu'il sort. Sinon un retour ultérieur dans le flow peut exécuter le callback plusieurs fois. 6. Tester toutes les branches succès backend; réponse vide ou utilisateur sans simulation/équipe; retour arrière / relog; professeur vs étudiant si le state est partagé; By Module vs By Year si le flow dépend du SimType; WebGL, où les erreurs de message sont plus sévèrement protégées. Debug d'un state bloqué Identifier la classe courante dans la machine. Mettre un breakpoint/log dans TransitionIn() pour confirmer qu'il n'est appelé qu'une fois. Inspecter chaque bool/condition testée dans Update(). Vérifier que le callback backend est réellement exécuté. Vérifier qu'un popup/tutoriel n'empêche pas la condition UI attendue. Si le state dépend du modèle, vérifier que le controller a reçu son RTT avant de changer de state. Controllers, modèles et listeners runtime Pattern général Le projet sépare les objets de données sérialisables des controllers runtime. Les classes de Assets/Src/Game/Data stockent les champs; les classes de Assets/Src/Game/Controllers ajoutent chargement, callbacks, caches, listeners RTT et opérations métier côté client. classDiagram class BaseData class BaseController class SimulationBase class SimulationBaseController class ActivityBase class ActivityBaseController class TeamBase class TeamBaseController BaseData <|-- SimulationBase BaseData <|-- ActivityBase BaseData <|-- TeamBase BaseController <|-- SimulationBaseController BaseController <|-- ActivityBaseController BaseController <|-- TeamBaseController SimulationBaseController --> SimulationBase ActivityBaseController --> ActivityBase TeamBaseController --> TeamBase BaseController Le constructeur appelle SetListeners(). La classe définit les hooks Deserialize, PreLoad, PostLoad, SetListeners et RemoveListeners. Une sous-classe qui change de contexte doit donc retirer ses listeners pour éviter les doubles callbacks. SimulationBaseController Ce controller est le hub runtime de la simulation. Il maintient notamment : simulation courante, activité et équipe courantes; stubs d'étudiants et équipes; permissions, ponderations et custom evaluations; présence/heartbeat; caches de stubs d'évaluation; listeners pour join/remove team, changement de nom, activité suivante, notifications, ponderations, etc. ActivityBaseController Il charge ActDataGameSpec, expose GenerateResults(), écoute ACT_UPDATED_FIELDS et sert de base aux variantes By Module et By Year. TeamBaseController Il porte les membres, activités/décisions, cash, logbook, résultats, last chat time et TeamEvaluation. Il écoute notamment les changements de cash, membres, logbook et création d'activité d'équipe. Factory Les controllers de base ne construisent pas directement leurs modèles. Ils passent par Factory.CreateSimulationBase(), CreateActivityBase(), CreateTeamBase(), etc. C'est ce qui permet aux deux familles de partager le même contrat runtime. Règle de maintenance Une donnée pure appartient au modèle; un accès backend, cache, listener ou événement UI appartient au controller. Éviter de mettre des appels BrainCloud directement dans les classes Data. Créer ou étendre un controller de A à Z Le controller est la couche qui transforme une donnée sérialisée en objet runtime vivant. Il peut charger le backend, maintenir des caches et écouter RTT. La donnée sérialisable elle-même reste dans Data/. 1. Créer/identifier le modèle Data Le modèle doit savoir Deserialize() ses champs sans appeler BrainCloud. Si une nouvelle propriété est persistée, ajouter d'abord le champ ici et vérifier sa clé JSON. 2. Créer le controller Hériter du controller de base le plus proche. Le constructeur crée normalement le modèle concret via Factory. 3. Respecter le cycle Deserialize → PreLoad → modèle → PostLoad Les controllers principaux suivent ce pattern pour invalider les caches et recalculer les dépendances autour d'une nouvelle payload. Ne pas mettre des listeners dans Deserialize(). 4. Ajouter les listeners symétriquement public override void SetListeners() { base.SetListeners(); Game.MessagesDispatcher.RegisterListener( MessageTypeBase.MY_EVENT, OnMyEvent ); } public override void RemoveListeners() { base.RemoveListeners(); Game.MessagesDispatcher.UnRegisterListener( MessageTypeBase.MY_EVENT, OnMyEvent ); } Toute inscription doit avoir sa désinscription miroir. Le remplacement d'une simulation appelle oldSim.RemoveListeners(); si une sous-classe oublie un listener, l'ancien controller continuera à recevoir des RTT. 5. Mettre à jour le modèle dans le handler avant de prévenir l'UI Le handler doit interpréter la payload, vérifier l'UID de simulation/activity/team concerné, mettre à jour le modèle/cache, puis déclencher les events locaux nécessaires. 6. Brancher Factory si la classe varie par famille/jeu Si le controller ou son modèle diffère entre By Module et By Year, ajouter la sélection dans Factory plutôt que des tests GameUid dispersés. Quand utiliser un cache Les stubs d'évaluation et listes lourdes sont mis en cache puis marqués dirty lorsqu'un RTT pertinent arrive. Appliquer ce pattern lorsqu'une donnée coûte cher à recharger mais peut être invalidée précisément. Checklist controller Modèle pur séparé du controller. Deserialize tolère les champs optionnels/anciens. Chaque RegisterListener possède UnRegisterListener. Le handler vérifie le contexte/UID avant de modifier l'état. Les caches sont invalidés sur tous les messages pertinents. Factory mise à jour si polymorphisme. Relog/changement de simulation ne produit pas de double callback. Simulations, activités, équipes et utilisateurs Hiérarchie métier commune flowchart TD USER[UserDetails] --> SIM[Simulation] SIM --> ACT[Activities] SIM --> ST[Students / Team stubs] ACT --> TEAM[Teams] TEAM --> MEMBER[TeamMember + Roles] ACT --> GS[ActDataGameSpec] TEAM --> DEC[Decisions / Modules] Simulation La simulation contient notamment teacher, étudiants, équipes, ordre des activités, activité courante, mode async, permissions, ponderations et custom evaluations. Les commandes communes couvrent création, lecture, modification, activation et récupération des stubs. Activity Une activité appartient à une simulation et possède son propre document activity, plus un document actDataGameSpec. Le contenu game-specific est volontairement séparé du socle commun. Team Le document teams contient membres, rôles, activités/décisions, cash et métadonnées runtime. By Module ajoute des documents teamsModules; By Year garde davantage de décisions annuelles dans l'état équipe/activité. Join / create / remove Les commandes communes incluent join_sim, join_team, create_team, remove_user_from_sim, remove_user_from_team et change_team_name. Ces opérations mettent à jour plusieurs documents et émettent des messages RTT. Rôles set_user_role, set_user_roles et unset_user_role modifient les rôles d'équipe. Le runtime Unity propage ces changements via TeamBaseController. Heartbeat / présence online Les controllers simulation/activity gardent les derniers heartbeats et considèrent un utilisateur online selon l'âge du dernier heartbeat. Les listeners RTT mettent à jour les statuts visibles. Écoles et access codes Le backend commun gère aussi école, teacher lookup et access codes. save_access_code / get_access_code persistent le code dans userDetails. Point de debug Lorsqu'un utilisateur “disparaît” ou reste dans une mauvaise équipe, comparer au minimum userDetails, simulation et teams. Le problème peut être une incohérence multi-document et non un simple cache Unity. Ajouter un champ de simulation/activity/team de A à Z Un nouveau champ traversera souvent plusieurs couches. Le piège classique est de l'ajouter au document serveur mais d'oublier le modèle Unity, ou l'inverse. 1. Choisir le bon document simulation : configuration globale qui survit aux activités; activity : état commun d'une activité; actDataGameSpec : données spécifiques au jeu pour une activité; teams : état de l'équipe; teamsModules : données détaillées By Module; userDetails : préférences/état du compte utilisateur. 2. Ajouter le champ au modèle Unity Ajouter la constante de clé et le champ dans la classe Data correspondante, puis le lire dans Deserialize(). Tolérer l'absence du champ pour les vieux documents lorsque nécessaire. 3. Ajouter l'écriture serveur Utiliser une commande dédiée ou une commande générique update_*_fields si le champ s'y prête. Sauvegarder via utils_documents. 4. Ajouter la migration si le champ est structurel Si le document peut exister longtemps, initialiser le nouveau champ dans version_simulation.js, version_activity.js, version_teams.js, etc. 5. Synchroniser les autres clients Ajouter un RTT *_updated_fields ou spécialisé. Le controller doit filtrer sur l'UID du document avant d'appliquer la mutation. 6. Tester tous les rôles Pour simulation/team, tester professeur + étudiant, join tardif et relog. Un client qui était offline doit récupérer la valeur correcte depuis le document, pas dépendre d'avoir reçu le RTT historique. Opérations multi-documents Join team, remove user, create team et suppression d'utilisateur touchent plusieurs documents. Pour toute nouvelle opération de ce type, écrire explicitement la liste des documents modifiés avant de coder. Un succès partiel peut laisser userDetails, simulation et teams incohérents. Debug d'une incohérence utilisateur/équipe Lire userDetails.teamUid/simUid/actUid. Lire simulation.students/ teams. Lire le document teams et ses members. Comparer les trois UIDs. Vérifier le dernier RTT join/remove. Recharger complètement la simulation : si le bug disparaît, suspecter le controller/cache plutôt que la persistance. Infrastructure runtime et utils Unity Dossier Assets/Src/Utils/ contient l'infrastructure Unity qui ne relève pas directement d'un domaine métier. Composant Rôle ContextState, State, Machine Fondations des machines d'état. ScheduledMachine, ContextScheduledMachine, Scheduler Orchestration d'états/tâches planifiées. Coroutiner Point d'accès aux coroutines pour les classes non-MonoBehaviour. GameEventMessageUtils Événements locaux UI/runtime distincts des messages RTT serveur. GlobalTime Temps serveur/global. SharedConstants GameUid, SimType et constantes transversales. Utils / UtilsBase Helpers client communs. Extensions Méthodes d'extension C#. ObjectPool, PoolableObject, PoolsManager Pooling d'objets Unity. ImageLoader Chargement d'images. SoundsManager Audio commun. TextsManager Localisation; documenté dans la page Textes. GameSkinReloader Rechargement du skin. ExcelUtils Helpers d'export tableur utilisés notamment en By Module/WebGL. Tweener Animation/tween utilitaire. HashInformation* Informations/erreurs liées aux contrôles d'intégrité. CommandManager CommandManager.cs est un gestionnaire historique des commandes GameSparks. Sa méthode SendCommand() refuse l'appel si le backend courant n'est pas eGameSparks. La voie BrainCloud moderne passe par BackEndBrainCloud.SendCommand(); ne pas confondre les deux systèmes. Événements locaux vs RTT flowchart LR LOCAL[GameEventMessageUtils] --> UI[UI / transitions locales] SERVER[BrainCloud RTT] --> DISPATCH[MessagesDispatcher] DISPATCH --> CTRL[Controllers] CTRL --> UI Un événement UI local n'est pas une synchronisation serveur. Pour une feature multi-utilisateur, il faut généralement persister côté serveur puis recevoir un message RTT. Règle de maintenance Éviter d'ajouter de grosses règles métier dans Utils.cs. Une fonction Accounting, MK1, HR1, etc. doit vivre avec son domaine. Le dossier Utils doit rester transversal. Carte réelle de l’infrastructure runtime Composant Rôle À utiliser pour Game Composition root Unity et accès aux services globaux. Services uniques réellement globaux au runtime. Scheduler Ordonnancement déterministe Update/LateUpdate par priorité. FSM et callbacks qui doivent respecter un ordre de frame. Coroutiner MonoBehaviour hôte pour coroutines lancées par du code non-MonoBehaviour. Backend IEnumerator, délais UI, séquences async Unity. ObjectPool / PoolsManager Réutilisation de prefabs/contrôles. Rows, pages, items et objets UI fréquemment créés/détruits. GameEventMessageUtils Bus d’événements Doozy local au client. Navigation/UI locale. MessagesDispatcher Distribution des messages RTT serveur. Synchronisation backend → client. Extensions Extensions génériques Action/UI/collections. Helpers sans état métier. UtilsBase Helpers bas niveau stables, temps/UIDs. Logique pure sans dépendance UI. Utils Helpers applicatifs historiques très larges. Compatibilité/usage existant; éviter d’y ajouter automatiquement toute nouvelle logique. CommandManager Throttling/loading des anciennes requêtes GameSparks. Legacy GameSparks seulement. Composition dans Game.Awake() Game est le composition root principal. Il initialise notamment : PoolsManager depuis une référence sérialisée; Scheduler = new Scheduler(); MessagesDispatcher = new MessagesDispatcher(); FxMgr, CommandManager, Toaster; NotificationManager; TextsManager; la machine d’état via ContextScheduledMachine. Game.Update() appelle Scheduler.Update(); Game.LateUpdate() appelle Scheduler.LateUpdate(). Un service enregistré au Scheduler n’a donc pas besoin de son propre MonoBehaviour.Update(). Ajouter un service runtime global de A à Z Ne rendre un service global que s’il doit réellement exister une seule fois pendant toute la session. Un service propre à une simulation, équipe ou popup doit rester dans son controller/contexte. 1. Choisir MonoBehaviour ou classe pure classe pure : pas besoin de scene lifecycle, inspector ou coroutine; MonoBehaviour : dépend de références Unity sérialisées, coroutines, callbacks Unity ou scene objects. 2. Ajouter la propriété dans Game seulement si elle est globale public static MyService MyService { get; private set; } Pour une classe pure, l’instancier dans Game.Awake(). Pour un MonoBehaviour, injecter une référence sérialisée depuis la scène/prefab puis assigner la propriété. 3. Définir explicitement son lifecycle Décider : quand il est initialisé; s’il dépend de Backend ou de CurrentGameUid; ce qui doit être reset lors d’un changement de simulation; ce qui doit être nettoyé dans OnDestroy() ou logout. 4. Ne pas créer un deuxième singleton caché Si le service est déjà accessible via Game, éviter un second static Instance sauf raison Unity spécifique. Les doubles singletons rendent les tests et changements de scène difficiles à tracer. 5. Tester relog / changement de jeu Un service global qui garde une référence au vieux TeamController, à une ancienne simulation ou à un prefab détruit produit des bugs fantômes. Tester au minimum : login → simulation A → logout/reload → simulation B. Scheduler — ordonnancement par priorité Les priorités sont exécutées dans cet ordre : BeginFrame ServerEvent Fsm World Render Ui EndFrame Le même ordre existe pour le registry de LateUpdate. Ajouter un callback planifié Game.Scheduler.Register( Scheduler.Priority.Ui, UpdateMyControl ); // nettoyage obligatoire Game.Scheduler.Unregister( Scheduler.Priority.Ui, UpdateMyControl ); Scheduler.Unregister() sait gérer une désinscription pendant qu’une priorité est en cours d’itération : le callback est placé dans _toRemove puis retiré après la boucle. Il faut néanmoins utiliser exactement le même delegate lors du unregister. ScheduledMachine ScheduledMachine.Start() s’enregistre automatiquement dans la priorité Fsm pour Update et LateUpdate. Stop() se désenregistre puis déclenche onStop. C’est la raison pour laquelle la machine d’état principale n’a pas son propre MonoBehaviour.Update(). Coroutiner — lancer une coroutine hors MonoBehaviour Game.Instance.coroutiner sert de point d’ancrage lorsque le code appelant n’est pas lui-même un MonoBehaviour : Coroutine c = Game.Instance.coroutiner.StartLocalCoroutine( MyRoutine() ); // si nécessaire Game.Instance.coroutiner.StopLocalCoroutine(c); Ne pas laisser une coroutine continuer après destruction logique de son owner. Si elle modifie un popup/controller qui peut disparaître, conserver la référence et l’arrêter au teardown. ObjectPool et PoolsManager ObjectPool possède deux modes : local pool — défaut actuel : le template et les instances restent gérés par le pool local; global PoolsManager — isLocalPool = false : le template est enregistré par son Type dans Game.PoolsManager. Créer un objet poolable public class MyRow : PoolableObject { public override void Alloc() { // remettre l’état nécessaire pour une nouvelle utilisation } public override void Free() { // retirer listeners, vider données temporaires, arrêter tweens } } Utilisation _pool = new ObjectPool(rowTemplate, contentRoot); MyRow row = _pool.AllocTemplate(); row.SetData(data); // plus tard _pool.FreeTemplate(row); // ou _pool.FreeAllTemplates(); Règle critique Free() doit remettre l’objet dans un état réutilisable. Un pool masque facilement des bugs de lifecycle : listener ajouté à chaque Alloc(), tween encore actif, référence à l’ancien modèle, event global non retiré. GameEventMessageUtils vs RTT vs Action locale Besoin Outil Changer un panel/navigation dans le même client GameEventMessageUtils.SendEvent(). Prévenir des objets C# directement liés Action/ event local. Synchroniser un changement venant du serveur MessagesDispatcher + RTT. Communiquer entre deux utilisateurs Serveur + persistance + RTT, jamais Doozy local. GameEventMessageUtils contient par exemple les événements de tabs, login/scenario/choose team et quelques événements By Module/MK. Ce bus ne persiste rien et ne traverse pas le réseau. Extensions.Fire() et diagnostic des callbacks Les extensions Action.Fire() ne sont pas de simples ?.Invoke() : elles parcourent la liste des delegates et encadrent chaque appel avec CrashMessages.CrashMessage.StackPush/StackPop. Cela permet de conserver plus de contexte lorsqu’un callback plante. Pour les events existants qui utilisent ce pattern, préférer .Fire() afin de conserver ce diagnostic. Temps : GlobalTime, UtilsBase et temps serveur GlobalTime.UtcNow retourne actuellement directement DateTime.UtcNow converti en epoch ms. L’ancien mécanisme de resynchronisation périodique serveur est commenté. En parallèle, Game.RefreshCurrentServerTime() charge explicitement l’heure via le backend et Game.Update() fait avancer currentServerTime. Donc, pour une règle métier sensible au serveur, ne pas supposer que GlobalTime.UtcNow est une horloge serveur synchronisée. Où placer un nouvel helper Type d’helper Emplacement recommandé Conversion temps/UID pure et partagée UtilsBase. Extension naturelle d’un type C# / Unity Extensions. Helper d’un domaine précis Classe/domain utils du domaine, pas Utils.cs. Helper UI très spécifique Contrôle/base UI concerné. Helper game-specific GameSpecific/. Compatibilité avec un ancien helper déjà centralisé Utils, seulement si cela reste cohérent avec l’existant. CommandManager : ne pas l’utiliser pour BrainCloud CommandManager.SendCommand() accepte des GSTypedRequest et vérifie explicitement que Game.Backend.BackEndType == eGameSparks. Il gère l’ancien throttling GameSparks et le loading circle. Une nouvelle feature BrainCloud doit passer par BackEndBrainCloud.SendCommand() / les méthodes de BackEndBase, jamais par CommandManager. Checklist avant d’ajouter une nouvelle infrastructure Le besoin est-il vraiment global ? Peut-il vivre dans un controller existant ? Le lifecycle de cleanup est-il défini ? Scheduler plutôt qu’un Update supplémentaire ? Coroutine arrêtée si owner détruit ? Pool : Alloc()/ Free() symétriques ? Event local ou RTT correctement choisi ? Helper placé dans le bon domaine plutôt que dans Utils.cs ? Aucune dépendance GameSparks introduite dans une nouvelle feature BrainCloud ? Relog/changement de simulation testé ? Debug rapide Symptôme Zone probable Callback exécuté deux fois après navigation listener/Scheduler non désinscrit ou pool Free() incomplet. Objet réutilisé avec anciennes données PoolableObject.Free/Alloc. FSM cesse d’avancer ScheduledMachine stoppée/désinscrite ou exception dans callback Fsm. UI locale ne bouge pas Doozy event / Action locale. Autre client ne bouge pas Ce n’est pas GameEventMessageUtils; suivre RTT/persistance. Heure différente du serveur GlobalTime vs Game.currentServerTime. Exception “Cannot use gamesparks” Nouvelle logique branchée par erreur sur CommandManager. Backend Unity ↔ BrainCloud Contrat côté Unity BackEndBase définit le contrat backend utilisé par le reste du client. Les controllers ne doivent pas connaître l'implémentation BrainCloud : ils appellent des opérations métier comme GetTeamModule, SaveModule, GenerateResults, GetEvaluation, etc. BackEndBrainCloud implémente ce contrat avec le SDK BrainCloud et les Cloud Scripts du dépôt Node. flowchart LR CTRL[Controller ou State Unity] --> BASE[BackEndBase] BASE --> BC[BackEndBrainCloud] BC -->|commun| C[_command/script] BC -->|game-specific| G[gameUid/command/gameUid_script] C --> U[_utils] G --> GU[utils game-specific] GU --> U Convention des Cloud Scripts Le helper privé BackEndBrainCloud.SendCommand() construit le chemin du script : commande commune : _command/; commande game-specific : /command/_. Exemple : SendCommand(true, "generate_results", ...) dans Acc1 exécute acc1/command/acc1_generate_results. Authentification et RTT sequenceDiagram participant G as Game participant B as BackEndBrainCloud participant BC as BrainCloud G->>B: Login(user, pass, gameUid...) B->>BC: AuthenticateUniversal BC-->>B: profile + session B->>B: EnableRTT() B-->>G: utilisateur authentifié Après authentification réussie, EnableRTT() est appelé. Le client conserve la fonction d'authentification pour pouvoir réagir à une perte de session et réactive RTT lorsqu'une connexion se ferme. GetDocument() GetDocument(entityType, uid, forceCreate) appelle _command/get_document. GetTeamModule() l'utilise pour lire teamsModules avec l'UID produit par UtilsBase.GetTeamModuleUid(teamUid, actModuleUid). Gestion des appels SendCommand() incrémente un compteur et pilote le loading circle. Les réponses sont désérialisées vers MessageData. Les erreurs sont transmises au callback d'échec. Une session invalide ou une connexion RTT morte peut déclencher une réauthentification ou une resynchronisation. Ajouter une commande Créer l'appel métier au bon controller/service. Ajouter le contrat à BackEndBase si l'opération doit faire partie de l'abstraction. Implémenter dans BackEndBrainCloud. Choisir commande commune ou game-specific. Créer le Cloud Script wrapper. Déplacer la logique réutilisable dans _utils. Sauvegarder les documents via utils_documents. Envoyer un message RTT si d'autres clients doivent être synchronisés. Ajouter une commande Unity → BrainCloud de A à Z Une nouvelle opération métier traverse normalement quatre couches : controller Unity → BackEndBase/BackEndBrainCloud → Cloud Script → utils/documents. Si plusieurs clients doivent voir le changement, ajouter ensuite RTT. 1. Définir le contrat Unity Ajouter la méthode dans BackEndBase lorsque l'opération appartient réellement au contrat backend. Utiliser des types simples/ MessageData compatibles avec les deux backends historiques. 2. Implémenter BackEndBrainCloud Construire la payload avec les UIDs minimaux nécessaires et appeler SendCommand(). isGameSpecific = false → _command/; isGameSpecific = true → /command/_. 3. Créer un wrapper Cloud Script mince Le fichier command doit valider/extraire data, inclure les utils nécessaires et déléguer le calcul. Une logique de centaines de lignes n'a pas sa place dans le wrapper. 4. Lire les documents avec utils_documents Utiliser getDocumentData() / gather helpers. Une modification doit conserver la version de Custom Entity et passer par saveDocumentData() ou l'helper adapté. 5. Sauvegarder avant d'envoyer RTT Le message temps réel doit représenter un état déjà persisté. Sinon un autre client peut recevoir le message, relire le document et obtenir encore l'ancienne valeur. 6. Choisir la stratégie RTT petite payload : envoyer directement les champs nécessaires; grosse payload : envoyer une instruction de refresh / get_data / execute_request selon le système; choisir user/team/team+teacher/sim/activity comme audience. 7. Mettre à jour le controller Unity Le callback de la commande est utile pour l'appelant local, mais les autres clients doivent passer par le listener RTT. Éviter deux chemins de mise à jour qui appliquent deux fois la même mutation. Exemple de checklist avant merge Contrat BackEndBase ajouté si requis. BackEndBrainCloud utilise le bon namespace common/game-specific. Wrapper command créé avec le nom exact attendu. Logic métier dans _utils ou game-specific utils. Document sauvegardé avec gestion de version. RTT envoyé après la sauvegarde. MessageType et listener Unity ajoutés si nécessaire. Chemin testé avec deux clients si la feature est collaborative. Server Script importé vers le bon environnement BrainCloud. Debug d'une commande Si Unity reçoit une erreur : logger scriptName et payload, exécuter le wrapper dans Debug local des Server Scripts BrainCloud, puis inspecter le document avant/après. Si la commande réussit mais l'autre client ne bouge pas, le problème est probablement dans la chaîne RTT plutôt que dans la commande. Commandes, documents et persistance BrainCloud Architecture serveur commune Le backend du Game repose principalement sur les Cloud Scripts dans node.js/backend/braincloud/server_scripts. Les dossiers _command, _admin_command, _job et _utils constituent le socle partagé. Familles de scripts Dossier Rôle _command/ Entrées appelées par le client : simulations, activités, équipes, évaluations, logbook, chat, rankings, static data, etc. _admin_command/ Outils administratifs comme création de documents en bulk et récupération de joueurs. _job/ Travaux planifiés : génération et migrations/versioning de documents. _utils/ Logique réutilisable : documents, messages, eval, rankings, RNG, By Year et By Module. /command/ Wrappers game-specific. utils_documents.js Ce fichier est la couche d'accès commune aux Custom Entities. Ses fonctions structurantes sont getDocumentData, gatherDocumentsData, gatherDocumentsDataWithQuery, saveDocumentData, saveDocumentDataFields, saveDocumentIncrementData, createDocumentData, deleteDocumentData, initializeDocument et updateDocumentVersion. sequenceDiagram participant CMD as Cloud Script participant UD as utils_documents participant CE as Custom Entity participant VER as version/* CMD->>UD: getDocumentData(type, uid) UD->>CE: lecture CE-->>UD: data + version UD->>VER: initialize / migrate UD-->>CMD: objet métier CMD->>CMD: modifications CMD->>UD: saveDocumentData(type, uid) UD->>CE: écriture Versioning et migrations Les scripts _utils/version/version_*.js portent les migrations par type de document. Chaque type peut enregistrer une fonction d'initialisation et une suite de transformations successives. version_teams_modules.js est un bon exemple : il contient plusieurs migrations teamModuleDataToV*. Un vieux document peut donc être transformé jusqu'au schéma actuel avant d'être resauvegardé. Documents principaux simulation : état/configuration de simulation; activity : activité en cours; actDataGameSpec : données propres au jeu pour l'activité; teams : état commun d'équipe; teamsModules : données By Module d'une équipe et d'un module; results, evaluation, evaluationTeam; teamsLogbooks, userDetails, simComments. Règle lorsqu'un schéma change Définir le nouveau schéma attendu. Ajouter la migration dans le bon version_.js. Garantir le passage de toutes les versions antérieures supportées. Tester lecture, migration et resauvegarde. Ne lancer un job de migration globale qu'après validation sur des documents représentatifs. Static data serveur _command/update_static_data.js est l'entrée utilisée par les outils d'export pour publier les JSON dans BrainCloud. La chaîne complète est documentée dans Données statiques et pipeline import/export. Diagnostic concurrence Un bug qui n'apparaît qu'avec plusieurs utilisateurs peut venir d'écritures concurrentes et de versions de Custom Entities. Dans ce cas, suivre le document concerné, sa version lue, la fonction de sauvegarde utilisée et les éventuels retries avant d'accuser l'UI. Ajouter ou faire évoluer une Custom Entity Le point critique n'est pas seulement de créer un nouveau JSON : il faut définir son UID, son propriétaire logique, son lifecycle, son versioning et tous les documents qui le référencent. Nouvelle entity type Choisir un entityType stable et un schéma de data.uid déterministe. Créer un helper d'initialisation si le document peut être absent. Passer les lectures/écritures par utils_documents.js. Ajouter un fichier version_.js si le schéma doit survivre à plusieurs versions du code. Brancher initializeDocument()/ updateDocumentVersion() pour ce type. Ajouter le document aux scripts de copie/migration si une simulation copiée doit l'emporter. Faire évoluer une entity existante Ne pas écrire du code qui suppose simplement que le nouveau champ existe. Ajouter une migration Vn → Vn+1 qui : tolère les documents anciens; initialise les nouveaux champs avec une valeur métier correcte; ne détruit pas les données inconnues; peut être appliquée dans l'ordre après toutes les migrations précédentes. Concurrence et version BrainCloud Une Custom Entity possède une version serveur. Une écriture issue d'une lecture ancienne peut être rejetée. Lorsqu'un bug n'apparaît qu'avec deux utilisateurs, logger au minimum : entityType, uid, version lue, version sauvegardée et commande à l'origine de l'écriture. Copie entre environnements exports/data_importer_braincloud.js copie plusieurs documents liés à une simulation : simulation, activity, results, actDataGameSpec, teams, teamsLogbooks, evaluationTeam, teamsModules, userDetails et evaluation. Si un nouveau type de document fait partie d'une simulation copiée, ce script doit être audité et potentiellement étendu. Checklist migration Créer un vieux document représentatif. Le charger avec le code actuel. Vérifier chaque transformation de version. Resauvegarder puis recharger. Tester une simulation active avec plusieurs clients. Tester le job global de migration seulement après validation ciblée. Tester la copie Dev → autre environnement si le document doit suivre une simulation. Référence des Cloud Scripts communs But Cette page est l'index des points d'entrée BrainCloud communs sous server_scripts/_command. Pour comprendre l'implémentation d'une commande, partir de ce wrapper puis suivre ses bridge.include() vers _utils. Simulation et activités create_simulation, get_simulation, get_simulations, get_active_simulations, get_active_simulations_simple, get_teacher_active_simulations, set_simulation_active, update_simulation_fields, create_activity, get_activity, get_activity_current_year, get_all_acts_stub, update_activity_fields, update_act_region, copy_act_modules, get_teacher_acts_and_modules. Utilisateurs, écoles et équipes create_user_details, get_user_details, update_user_details_fields, get_display_name, edit_student, get_student_stub, get_simulation_students, get_teachers, get_schools, set_school, join_sim, join_team, remove_user_from_sim, remove_user_from_team, get_team, get_teams_list, change_team_name. Rôles, état utilisateur et présence set_user_role, set_user_roles, unset_user_role, set_user_state, set_presence, set_language, set_volume, set_has_image, set_last_chat_time. Évaluations get_evaluation, get_student_evaluation_stub, get_sim_evaluations_stubs, get_act_perf_evaluation, ignore_year_from_average, save_act_perf_ponderation, save_ponderation, save_overrided_eval_for_student, get_presence_evaluation, get_scenario_evaluation, save_eval_segment, save_segment. Custom evaluations et pair evaluation create_custom_eval, get_custom_evaluation, save_custom_eval, update_custom_eval_fields, delete_custom_eval, get_pair_eval, save_pair_eval, teacher_get_pair_eval. Logbook, chat et commentaires get_logbook, save_logbook, restore_logbook_history, get_logbook_evaluation, save_logbook_eval, save_logbook_ponderation_type, get_chats, add_chat, save_sim_comment, get_sim_comment, get_sim_comments. Scénarios et tutoriels save_scenario_answers, set_scenario_step, set_year_sequence_seen, skip_the_tutorial. Documents et données get_document, gather_documents, get_data, get_act_data_game_spec, update_static_data, get_results, get_time. By Module exposé dans le socle save_module, remove_module, save_module_additional_time, restore_module_backup. By Year exposé dans le socle change_year_state, student_take_decisions, save_market_size_modifier. Les wrappers game-specific de génération/réouverture sont documentés dans By Year. Rankings generate_all_rankings, get_global_rankings, get_school_rankings, get_sim_rankings. Access code et device/linking save_access_code, get_access_code, get_link_device_info, get_link_device_request. Autres intégrations send_message_to_teacher_web relie certains événements au backend Web. ai_create_and_launch_product expose une action d'aide/automatisation produit utilisée dans le contexte marketing. Admin commands _admin_command/create_documents_in_bulk.js _admin_command/get_or_create_user.js _admin_command/get_player_names.js Jobs _job/_scheduled_job.js : scheduler commun; _job/generate_results_job.js : génération planifiée By Year; _job/update_documents_versions_job.js : mise à jour de versions de documents; _job/utils/update_documents_version.js : logique utilitaire de migration. Règle de lecture flowchart LR UNITY[BackEndBrainCloud method] --> CMD[_command/*.js] CMD --> INCLUDE[bridge.include] INCLUDE --> UTILS[_utils / game-specific utils] UTILS --> DOC[Custom Entities] UTILS --> MSG[RTT messages] Référence des utils serveur BrainCloud Carte générale Fichier/dossier Responsabilité utils.js Helpers transversaux, UIDs, calculs simples, copie/merge, équipes/étudiants, rôles, CRC, logging. utils_documents.js Lecture, query, création, sauvegarde, suppression, increments et versioning des Custom Entities. utils_message.js Envoi de messages RTT à user/team/sim/activity et messages spécialisés. utils_eval.js Stubs et scores d'évaluation communs. utils_rankings.js Scores, tops et mise à jour des rankings. utils_RNG.js Random déterministe utilisé par la génération serveur. utils_redis.js Helpers d'accès Redis lorsque la feature l'utilise. utils_game_specific_base.js Base des utils game-specific et accès aux static data/chunks. utils_by_module.js Charge/initialise le socle By Module. utils_by_year.js Cycle annuel, jobs, bots et transitions By Year. by_module/ Moteurs Accounting : création data, bills, answer key, grading, results, inventory. command/ Implémentations réutilisables appelées par les wrappers command. factory/ Sélection game-specific des utils, évaluations et générateurs de résultats. version/ Migrations par type de document. utils.js — fonctions structurantes Parmi les helpers importants : getTeamActUid, getActUid, dates/mois/années, deleteTeam, getDisplayName, getActEvalTeamData, getTeamDecisionsData, getTeamsOfActivity, getStudentsOfActivity, hasRole, getUserUid, mergeObjects, copyOf/deepCopyOf, logging Discord et validation CRC avant sauvegarde équipe. command/ utils_create_activity.js : logique partagée de création d'activité; utils_create_team.js : création/initialisation d'équipe; utils_custom_command_by_module.js : routeur By Module; utils_custom_command_by_year.js : routeur By Year; utils_join_simulation.js : logique de join simulation; utils_reopen_module.js : rollback/réouverture By Module; utils_reopen_year.js : rollback/réouverture By Year; utils_start_next_activity.js : transition d'activité. factory/ utils_factory_game_specific_utils.js : instancie utilsByGameUid; utils_factory_generate_results.js : moteur de résultats par jeu; utils_factory_eval.js : moteur d'évaluation par jeu. Principe de placement Un wrapper sous _command doit rester mince. Dès qu'une logique est partagée entre plusieurs entrées, elle doit être placée dans _utils ou dans la famille métier adéquate. Les règles propres à ENT1/MK1/etc. restent dans leurs dossiers game-specific. RTT, messages et synchronisation Chaîne de synchronisation BrainCloud RTT permet au serveur d'avertir les clients déjà connectés lorsqu'un document ou un état métier change. Côté serveur, la couche commune est _utils/utils_message.js. Côté Unity, BackEndBrainCloud reçoit les messages et les distribue au runtime. sequenceDiagram participant S as Cloud Script participant M as utils_message participant RTT as BrainCloud RTT participant B as BackEndBrainCloud participant D as MessagesDispatcher participant C as Controller S->>M: sendMessageToTeam / User / Sim M->>RTT: message type + data RTT-->>B: OnRTTMessageReceived B->>D: ServerMessage D-->>C: listener C->>C: refresh du modèle / UI Helpers serveur utils_message.js expose notamment sendMessageToTeam, sendMessageToMemberList, sendMessageToSim, sendMessageToActStudents, sendMessageToUser, sendMessageToTeamAndTeacher et des messages spécialisés comme sendLedgerPageChangedMessage. Connexion RTT Après AuthenticateUniversal, BackEndBrainCloud.EnableRTT() appelle BrainCloud.RTTService.EnableRTT. Sur succès, le client enregistre RegisterRTTMessagingCallback(OnRTTMessageReceived). Si la socket est fermée, le code tente de réactiver RTT; si l'authentification n'est plus valide, il réutilise la fonction d'authentification. Règle pour une nouvelle feature Une écriture serveur réussie ne suffit pas si plusieurs utilisateurs voient le même état. Après la sauvegarde, déterminer qui doit être averti : utilisateur, équipe, équipe + professeur, activité ou simulation. Le listener Unity doit ensuite mettre à jour le modèle local avant l'UI. Diagnostic Vérifier que le Cloud Script envoie bien un message. Vérifier le type et la payload. Vérifier que RTT est connecté. Trouver le MessageType côté Unity. Trouver le listener dans le controller. Vérifier si le listener applique la donnée reçue ou redemande un document. Ajouter un nouveau message RTT de A à Z 1. Définir le message côté Unity Ajouter la constante dans MessageTypeBase.cs si le message est commun, ou MessageTypeByModule.cs s'il appartient au framework By Module. La string doit être exactement la même que celle envoyée par le serveur. 2. Envoyer depuis le serveur après la sauvegarde Utiliser l'helper d'audience approprié dans utils_message.js : sendMessageToUser; sendMessageToTeam; sendMessageToTeamAndTeacher; sendMessageToSim; sendMessageToActStudents. saveDocumentData("teams", teamData.uid); sendMessageToTeamAndTeacher(teamData, "my_data_changed", { teamUid: teamData.uid, value: teamData.myValue }); 3. Enregistrer le listener dans le controller public override void SetListeners() { base.SetListeners(); Game.MessagesDispatcher.RegisterListener( MessageTypeBase.MY_DATA_CHANGED, OnMyDataChanged ); } public override void RemoveListeners() { base.RemoveListeners(); Game.MessagesDispatcher.UnRegisterListener( MessageTypeBase.MY_DATA_CHANGED, OnMyDataChanged ); } 4. Traiter la payload Le handler vérifie l'UID/context, applique les données au modèle puis déclenche l'event UI nécessaire. Ne pas recharger tout le document si quelques champs suffisent. 5. Grosse payload : ne pas l'envoyer en RTT Pour des résultats/module data volumineux, envoyer une instruction de récupération. Le code By Module utilise par exemple execute_request pour demander au client de récupérer les résultats plutôt que d'envoyer tout le résultat dans le message. Ordonnancement réel côté client BackEndBrainCloud.OnRTTMessageReceived() transforme la payload en ServerMessage. MessagesDispatcher met les messages dans une queue, attend brièvement, les trie par timestamp puis appelle chaque listener enregistré. Ce buffer réduit les problèmes d'ordre entre messages proches. Le cas get_data OnRTTMessageReceived() reconnaît les messages qui demandent un fetch supplémentaire. Dans ce cas le client charge la donnée avant de construire le ServerMessage final. Utiliser ce pattern lorsque la source de vérité doit rester le document serveur. Test minimal multi-client Ouvrir deux clients sur la même équipe/simulation. Faire l'action sur A. Vérifier que le document est sauvegardé. Vérifier le log RTT de B. Vérifier que le bon listener B est exécuté une seule fois. Changer de simulation sur B puis refaire l'action : aucun ancien controller ne doit encore recevoir le message. Système d’évaluation commun Couche commune Les deux familles utilisent un socle d'évaluation partagé : StudentEvaluationBase, TeamEvaluationBase, leurs controllers, les stubs et les commandes BrainCloud communes. Sources de notes performance d'activité; présence; logbook; scenario / segments; pair evaluation; custom evaluations; teacher overrides; By Module : scores de modules; By Year : scores annuels game-specific. Commandes communes Le contrat backend inclut notamment GetEvaluation, GetStudentEvaluationStub, GetActPerfEvaluation, GetLogbookEvaluation, GetCustomEvaluation, GetPresenceEvaluation, création/modification/suppression de custom eval, pair eval et teacher overrides. Pondérations La simulation conserve les ponderations d'évaluation et d'ActPerf. Les changements sont persistés serveur puis propagés en RTT afin que les contrôles professeur/étudiant se mettent à jour. Stubs SimulationBaseController maintient des caches de EvalStudentStub et EvalTeamStub. Ils sont marqués dirty lorsque certains messages arrivent afin d'éviter de recharger toutes les évaluations en permanence. Spécialisation flowchart TD BASE[Évaluation commune] --> BM[By Module] BASE --> BY[By Year] BM --> MOD[Module scores / correction] BY --> YR[Year results / game-specific Eval] BASE --> LOG[Logbook] BASE --> PRES[Presence] BASE --> CUST[Custom / Pair / Overrides] Règle de maintenance Les calculs propres à un jeu restent dans son moteur Eval; la composition, les ponderations, les stubs et les mécanismes de sauvegarde/override restent communs. Ajouter une nouvelle composante d’évaluation Avant d'ajouter une note, décider si elle appartient à evaluation (étudiant), evaluationTeam (équipe), ActPerf, ou si elle est seulement un détail de résultat game-specific. 1. Définir la source de vérité score individuel → document evaluation; score d'équipe/performance → evaluationTeam; score de module → généré dans le pipeline By Module puis propagé vers evaluationTeam.actPerf; score annuel game-specific → calcul du moteur Eval du jeu. 2. Définir la pondération Les évaluations supportent plusieurs stratégies : annuelle/global, individuelle/équipe. Pour ActPerf, getActPerfScore() utilise la pondération enregistrée dans la simulation ou, à défaut, la static data act_perf_pond. 3. Sauvegarder la note Ajouter/adapter la commande serveur responsable. Après sauvegarde, envoyer le stub RTT approprié plutôt que de forcer tous les clients à recharger toutes les évaluations. 4. Ajouter le calcul de stub utils_eval.js contient les fonctions de score compact : getActPerfScore, getModuleTypeScore, getPresenceScore, getLogbookScore, getCustomEvalScore et les builders de stubs étudiant/équipe. 5. Invalider les caches Unity SimulationBaseController et les controllers d'évaluation cachent certains stubs. Le nouveau message doit soit mettre à jour directement la valeur, soit marquer le cache dirty. Valeur -1 : signification importante Plusieurs calculs retournent -1 pour signifier “ignorer / pas encore évalué”, tandis que 0 signifie réellement une note nulle. Ne pas convertir automatiquement un -1 en 0 dans l'UI ou dans un nouvel agrégat. Checklist évaluation Document correct choisi. Pondération et type annuel/global définis. Cas ongoing vs finished testé. -1 conservé lorsque la note doit être ignorée. RTT/stub envoyé. Cache Unity invalidé. Export Excel/rapport vérifié si la nouvelle note doit y apparaître. By Year sync, async et By Module testés selon le scope. Données statiques et pipeline import/export Vue complète La static data n'est pas maintenue uniquement à la main dans Unity. Le dépôt contient un pipeline qui peut lire Google Sheets, générer les JSON du client puis publier la même donnée dans BrainCloud. flowchart LR S[Google Sheets] --> I[exports/importer.js] I --> J[Assets/GameSpecific/.../Resources/data_*/*.json] J --> E[exports/exportAllData.js] E --> BCE[BrainCloudExporter.js] BCE --> CMD[_command/update_static_data] CMD --> BC[(Static Data BrainCloud)] J --> U[Unity Resources] importer.js exports/importer.js télécharge les feuilles via Google Sheets API, nettoie les headers, infère ou force les types, convertit string/double/long/bool/json et écrit les JSON. Un header peut forcer son type avec une notation comme (str), (long), (double), (bool) ou (json). exportAllData.js exports/exportAllData.js contient la carte des jeux, Spreadsheet IDs, GIDs et fichiers additionnels. Il connaît les jeux By Year, Acc1/Acc2, le dataset partagé byModule, les tables fiscales annuelles et les données de bots. Pour un jeu Accounting, la pipeline met à jour les données propres au jeu puis le dataset partagé byModule. Les données sont écrites sous ../game/Assets/GameSpecific//Resources/data_/. Publication BrainCloud BrainCloudExporter.exportJson() initialise un wrapper BrainCloud et appelle _command/update_static_data. Certaines sources utilisent separateTabs et un collectionType dédié, notamment les chunks fiscaux. Données By Module partagées Le dataset byModule inclut notamment accounts, account types, taxes, employer taxes, interests, immobilizations, contract regularizations, ainsi que des fichiers additionnels comme modules, module_pages, regions_settings, act_settings, modules_settings, pages_settings et modules_contexts. Checklist de modification Identifier si la source officielle est Google Sheets ou un JSON additionnel. Modifier la source correcte. Importer/générer le JSON. Inspecter le diff du JSON Unity. Publier vers le bon environnement BrainCloud si nécessaire. Tester le chargement Unity et le runtime serveur. Pour By Module, vérifier aussi les filtres régionaux et settings décrits dans la page de configuration Accounting. Ajouter un nouveau dataset Google Sheets de A à Z 1. Définir la source dans exportAllData.js Ajouter le mapping du jeu vers la Spreadsheet et le GID/tab attendu. Si la donnée appartient au socle Accounting partagé, déterminer si elle doit vivre sous byModule plutôt que sous acc1/ acc2. 2. Définir le nom du JSON Unity Le nom doit correspondre à ce que StaticData*.Initialize() charge. Une nouvelle feuille importée mais jamais référencée par le loader ne sera jamais utilisée. 3. Définir les types de colonnes Dans Google Sheets, utiliser les suffixes forcés (str), (long), (double), (bool) ou (json) lorsque l'inférence est ambiguë. Une erreur ici peut produire un JSON valide mais un mauvais type runtime. 4. Importer localement Depuis node.js/exports/ : importDataGoogleSheet.bat # ou directement node exportAllData.js importAllGoogleSheet Le batch valide une liste de GameUid; si un nouveau GameUid/dataset n'y apparaît pas, mettre aussi ce batch à jour. 5. Inspecter le diff JSON Ne jamais pousser l'export sans regarder le diff. Vérifier notamment UID, types numériques, booléens, tableaux JSON, lignes supprimées et ordre des données importantes. 6. Publier dans BrainCloud exportToBrainCloud.bat # ou node exportAllData.js exportAllBrainCloud Le chemin passe par BrainCloudExporter.js puis _command/update_static_data. 7. Valider les deux côtés Unity : le JSON est présent dans le bon Resources/data_* et le controller le charge. Serveur : utils_game_specific_base/ utilsByGameUid retourne la nouvelle donnée. Accounting : tester les overrides région/module/page si le dataset est filtré par settings. Ne pas éditer le JSON généré comme source officielle Si le fichier vient d'une Google Sheet, une modification manuelle du JSON sera écrasée au prochain import. Modifier la Sheet puis regénérer. Les seuls JSON maintenus directement sont ceux explicitement traités comme fichiers additionnels par exportAllData.js. Textes, localisation et mise à jour Pipeline flowchart LR S[Google Sheets] --> I[importUploadTexts.js] I --> JSON[shared_texts / gameUid_texts] JSON --> UNITY[TextsManager] JSON --> WEB[Upload serveur web] WEB --> API[/getTexts/gameUid/lang/timestamp] API --> UNITY UNITY --> TL[TextLoader / UI] Fichiers générés partagés : Assets/Resources/data/shared_texts_fr.json et shared_texts_en.json; jeu : Assets/GameSpecific//Resources/data_/_texts_fr.json et EN. Import Node exports/importUploadTexts.js associe chaque GameUid à ses feuilles Google et appelle importer.importTexts(). Les commandes exposées permettent l'import game-specific, shared, ou l'import suivi de l'upload. Pour Accounting, le script traite Acc1 et Acc2 ensemble via accountingTextGameUids = ["acc1", "acc2"], afin d'éviter de mettre à jour une seule variante. Chargement Unity TextsManager.Initialize() charge les textes shared. Game.SetGameUid() appelle LoadGameSpecificTexts(). SetLang() recharge les deux couches puis appelle TextLoader.ReloadAll(). Un UID absent retourne [uid]. Mise à jour sans rebuild UpdateTextsFromWebServer() interroge /getTexts///. Le client garde timestamp/hash et peut donc recevoir des textes plus récents que ceux embarqués dans le build. Modifier un texte Trouver l'UID utilisé par TextLoader ou Game.Texts.GetText(). Modifier la source Google Sheets appropriée. Lancer l'import. Vérifier FR + EN. Publier si le changement doit être disponible sans rebuild. Sinon inclure les JSON générés dans le prochain build Unity. Procédure exacte pour ajouter ou modifier un texte Choisir un UID stable; ne pas utiliser la phrase FR elle-même comme identifiant. Ajouter/modifier FR et EN dans la Google Sheet de textes appropriée. Depuis node.js/exports/, lancer importTexts.bat et entrer le GameUid, ou shared pour les textes communs. Inspecter les JSON générés. Si le texte doit être disponible sans rebuild, lancer importThenUploadTextsToUniksimServer.bat. Tester le changement de langue dans un client déjà ouvert via TextsManager.SetLang(). Commandes directes équivalentes node importUploadTexts.js shared node importUploadTexts.js game_specific mk1 node importUploadTexts.js uploadTexts mk1 Cas Accounting getGameUidsToImport() traite Accounting comme une paire : un import de textes Acc1/Acc2 met à jour les deux jeux concernés. Ne pas maintenir deux traductions divergentes par erreur. Choisir shared ou game-specific texte réutilisé par plusieurs jeux/UI commune → shared; vocabulaire/règle propre à un jeu → game-specific; ne pas copier un même UID dans plusieurs couches juste pour contourner un problème de chargement. Debug d'un UID affiché comme [my_uid] Vérifier que l'UID exact existe dans la Sheet. Vérifier que le JSON correspondant a été regénéré. Vérifier shared vs data_. Vérifier la locale courante. Si le serveur web doit fournir la mise à jour, vérifier l'upload et le timestamp/hash. Si le texte est dans Resources seulement, rebuilder Unity. UI, prefabs et composants récurrents Frontière Cette page décrit uniquement les briques UI réellement communes. Les écrans Data-Driven Accounting vont dans By Module; les écrans de cycle annuel vont dans By Year; les prefabs propres à un jeu restent dans sa page game-specific. Dossiers Assets/Src/UI/ : base commune des contrôles; Assets/Src/UI/SimByModule/ : spécialisation By Module; Assets/Src/UI/SimByYear/ : spécialisation By Year; Assets/GameSpecific//Resources/Prefabs/UI/ : prefabs propres au jeu. Composants communs importants Famille Classes représentatives Navigation / panels MainControlStudent, MainControlTeacher, MainContentPanel, SimPanel, SubPanelBase Popups / wizards WizardPopupControlBase, WizardPageControlBase, YesNoPopupControl, popups d'édition Inputs FormattedTMPInputFieldBase, inputs nombres/percentages/positifs, date picker, autocomplete Sélecteurs SelectorControl, grouped/non-grouped, dropdown overlays, toggles Profil / équipes ProfilePopupControl, StudentCardControl, GroupStudentsControl, GroupTeamsControl Communication ChatPopupControl, NotificationsListPopupControl, toasters Scénario ScenarioPanelControlBase, ScenarioPopupControl, ScenarioItemControl Tooltips / tables Tooltip base, table rows/cells, scroll helpers et layout helpers Règle de prefab Un prefab commun doit rester générique. S'il commence à contenir des références spécifiques à Accounting, MK1, HR1, etc., créer/maintenir une variante dans le dossier approprié plutôt que d'ajouter des branches GameUid au prefab commun. Diagnostic UI Identifier le prefab instancié. Repérer le contrôle C# attaché. Remonter vers sa classe Base. Identifier le controller/model qui fournit les données. Vérifier TextLoader si le souci est textuel. Vérifier le message RTT si l'UI est seulement désynchronisée. Créer un composant UI commun de A à Z 1. Vérifier qu'il est réellement commun Si le contrôle connaît un modulePageUid, un segment MK, un employee HR ou une règle ENT, il n'est probablement pas commun. Le mettre dans la famille ou le GameSpecific concerné. 2. Créer le script de contrôle Placer le C# sous Assets/Src/UI/ dans le dossier logique existant. Exposer les références Unity avec [SerializeField] lorsque possible plutôt que des Find() runtime. 3. Créer/modifier le prefab Le prefab réutilisable doit vivre dans les Resources/prefabs communs adaptés. Ne pas modifier une instance de scène sans propager les changements au prefab. 4. Brancher les textes Utiliser TextLoader ou Game.Texts; ne pas hardcoder un texte utilisateur dans le prefab. Ajouter FR/EN via le pipeline Textes. 5. Ajouter les events/listeners Les listeners de buttons doivent être ajoutés dans Start/OnEnable selon le lifecycle du contrôle et retirés dans OnDestroy/OnDisable lorsqu'ils ciblent des events statiques/globaux. 6. Si le composant doit être ciblable par un tutoriel Ajouter TutorialUIObject ou TutorialUIButton au prefab avec un UID stable. Voir la page Tutoriels — création de A à Z et architecture. 7. Si la donnée est live L'UI ne devrait pas écouter BrainCloud directement. Le controller reçoit le RTT, modifie le modèle, puis l'UI observe l'event/controller. Checklist prefab références sérialisées non null; anchors/Canvas adaptés aux résolutions WebGL; TextLoader/UIDs présents; aucune dépendance game-specific cachée; listeners nettoyés; état initial correct lors d'une réouverture du popup/panel; test dans prefab variant et scene réelle; build si prefab/asset modifié. Debug UI Si l'UI affiche une mauvaise valeur : inspecter d'abord le modèle/controller. Si le modèle est correct, inspecter le binding/control. Si le modèle est faux seulement en live, suivre le RTT. Si le contrôle fonctionne en Editor mais pas WebGL, vérifier les références sérialisées et les exceptions dans la console navigateur. 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//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