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

Points d'entrée Node / BrainCloud

Méthode pour tracer une feature

  1. Identifier le contrôle UI et le controller Unity.
  2. Trouver la méthode correspondante dans BackEndBase/BackEndBrainCloud.
  3. Identifier le Cloud Script commun ou game-specific réellement appelé.
  4. Suivre ses bridge.include().
  5. Identifier chaque document lu ou sauvegardé.
  6. Identifier le message RTT envoyé.
  7. Retrouver le listener Unity qui consomme ce message.
  8. 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) :

  1. convertit le string en SharedConstants.GameUid.eGameUid;
  2. renseigne CurrentGameUidEnum et CurrentGameUid;
  3. détermine CurrentSimType avec SharedConstants.SimType.GetSimTypeFromGameUid();
  4. charge les textes game-specific;
  5. crée le controller de static data via Factory.CreateStaticDataController();
  6. 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.

GameUidFamilleStaticDataController
ent1By YearStaticDataControllerEnt1
mk1By YearStaticDataControllerMk1
hr1By YearStaticDataControllerHr1
mk2By YearStaticDataControllerMk2
mgt1By YearStaticDataControllerMgt1
acc1By ModuleStaticDataControllerByModule / spécialisation Acc1
acc2By ModuleStaticDataControllerByModule / 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

  1. Ajouter le GameUid dans SharedConstants.
  2. Mapper le GameUid vers le bon SimType.
  3. Ajouter les classes concrètes nécessaires dans Factory.
  4. Créer Assets/GameSpecific/<gameUid> et ses Resources.
  5. Ajouter les static data/textes dans les pipelines Node.
  6. Ajouter les Server Scripts game-specific et les factories serveur si la logique diffère.
  7. 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

Debug d'un state bloqué

  1. Identifier la classe courante dans la machine.
  2. Mettre un breakpoint/log dans TransitionIn() pour confirmer qu'il n'est appelé qu'une fois.
  3. Inspecter chaque bool/condition testée dans Update().
  4. Vérifier que le callback backend est réellement exécuté.
  5. Vérifier qu'un popup/tutoriel n'empêche pas la condition UI attendue.
  6. 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 :

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

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

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

  1. Lire userDetails.teamUid/simUid/actUid.
  2. Lire simulation.students/teams.
  3. Lire le document teams et ses members.
  4. Comparer les trois UIDs.
  5. Vérifier le dernier RTT join/remove.
  6. 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<T> / 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 :

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

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 :

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<T> possède deux modes :

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<MyRow>(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/<gameUid>.
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

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 :

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

Ajouter une commande

  1. Créer l'appel métier au bon controller/service.
  2. Ajouter le contrat à BackEndBase si l'opération doit faire partie de l'abstraction.
  3. Implémenter dans BackEndBrainCloud.
  4. Choisir commande commune ou game-specific.
  5. Créer le Cloud Script wrapper.
  6. Déplacer la logique réutilisable dans _utils.
  7. Sauvegarder les documents via utils_documents.
  8. 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().

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

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

  1. Contrat BackEndBase ajouté si requis.
  2. BackEndBrainCloud utilise le bon namespace common/game-specific.
  3. Wrapper command créé avec le nom exact attendu.
  4. Logic métier dans _utils ou game-specific utils.
  5. Document sauvegardé avec gestion de version.
  6. RTT envoyé après la sauvegarde.
  7. MessageType et listener Unity ajoutés si nécessaire.
  8. Chemin testé avec deux clients si la feature est collaborative.
  9. 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.
<gameUid>/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

Règle lorsqu'un schéma change

  1. Définir le nouveau schéma attendu.
  2. Ajouter la migration dans le bon version_<entity>.js.
  3. Garantir le passage de toutes les versions antérieures supportées.
  4. Tester lecture, migration et resauvegarde.
  5. 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

  1. Choisir un entityType stable et un schéma de data.uid déterministe.
  2. Créer un helper d'initialisation si le document peut être absent.
  3. Passer les lectures/écritures par utils_documents.js.
  4. Ajouter un fichier version_<entity>.js si le schéma doit survivre à plusieurs versions du code.
  5. Brancher initializeDocument()/updateDocumentVersion() pour ce type.
  6. 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 :

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

  1. Créer un vieux document représentatif.
  2. Le charger avec le code actuel.
  3. Vérifier chaque transformation de version.
  4. Resauvegarder puis recharger.
  5. Tester une simulation active avec plusieurs clients.
  6. Tester le job global de migration seulement après validation ciblée.
  7. 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

Jobs

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/dossierResponsabilité
utils.jsHelpers transversaux, UIDs, calculs simples, copie/merge, équipes/étudiants, rôles, CRC, logging.
utils_documents.jsLecture, query, création, sauvegarde, suppression, increments et versioning des Custom Entities.
utils_message.jsEnvoi de messages RTT à user/team/sim/activity et messages spécialisés.
utils_eval.jsStubs et scores d'évaluation communs.
utils_rankings.jsScores, tops et mise à jour des rankings.
utils_RNG.jsRandom déterministe utilisé par la génération serveur.
utils_redis.jsHelpers d'accès Redis lorsque la feature l'utilise.
utils_game_specific_base.jsBase des utils game-specific et accès aux static data/chunks.
utils_by_module.jsCharge/initialise le socle By Module.
utils_by_year.jsCycle 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/

factory/

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

  1. Vérifier que le Cloud Script envoie bien un message.
  2. Vérifier le type et la payload.
  3. Vérifier que RTT est connecté.
  4. Trouver le MessageType côté Unity.
  5. Trouver le listener dans le controller.
  6. 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 :

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

  1. Ouvrir deux clients sur la même équipe/simulation.
  2. Faire l'action sur A.
  3. Vérifier que le document est sauvegardé.
  4. Vérifier le log RTT de B.
  5. Vérifier que le bon listener B est exécuté une seule fois.
  6. 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

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é

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

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/<gameUid>/Resources/data_<gameUid>/.

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

  1. Identifier si la source officielle est Google Sheets ou un JSON additionnel.
  2. Modifier la source correcte.
  3. Importer/générer le JSON.
  4. Inspecter le diff du JSON Unity.
  5. Publier vers le bon environnement BrainCloud si nécessaire.
  6. Tester le chargement Unity et le runtime serveur.
  7. 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 <gameUid>

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

Le chemin passe par BrainCloudExporter.js puis _command/update_static_data.

7. Valider les deux côtés

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

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/<gameUid>/<locale>/<timestamp>. Le client garde timestamp/hash et peut donc recevoir des textes plus récents que ceux embarqués dans le build.

Modifier un texte

  1. Trouver l'UID utilisé par TextLoader ou Game.Texts.GetText().
  2. Modifier la source Google Sheets appropriée.
  3. Lancer l'import.
  4. Vérifier FR + EN.
  5. Publier si le changement doit être disponible sans rebuild.
  6. Sinon inclure les JSON générés dans le prochain build Unity.

Procédure exacte pour ajouter ou modifier un texte

  1. Choisir un UID stable; ne pas utiliser la phrase FR elle-même comme identifiant.
  2. Ajouter/modifier FR et EN dans la Google Sheet de textes appropriée.
  3. Depuis node.js/exports/, lancer importTexts.bat et entrer le GameUid, ou shared pour les textes communs.
  4. Inspecter les JSON générés.
  5. Si le texte doit être disponible sans rebuild, lancer importThenUploadTextsToUniksimServer.bat.
  6. 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

Debug d'un UID affiché comme [my_uid]

  1. Vérifier que l'UID exact existe dans la Sheet.
  2. Vérifier que le JSON correspondant a été regénéré.
  3. Vérifier shared vs data_<gameUid>.
  4. Vérifier la locale courante.
  5. Si le serveur web doit fournir la mise à jour, vérifier l'upload et le timestamp/hash.
  6. 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

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

  1. Identifier le prefab instancié.
  2. Repérer le contrôle C# attaché.
  3. Remonter vers sa classe Base.
  4. Identifier le controller/model qui fournit les données.
  5. Vérifier TextLoader si le souci est textuel.
  6. 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

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/<gameUid>/Src/Tutorial/ ou dossier UI du jeu si c'est déjà la convention de ce jeu.
Tutoriel joué une seule fois par utilisateur Le déclenchement doit tester un Scenario.eScenarioStep, puis appeler SetScenarioStep() lorsque le tutoriel est réellement complété.
État propre à une simulation plutôt qu'au compte utilisateur global Utiliser le système UserState plutôt qu'ajouter automatiquement un ScenarioStep.

Architecture réelle

flowchart TD
TRIGGER[GameState / UI / événement] --> SEQ[MenuTutorialSequence]
SEQ --> CHARACTER[TutorialCharacter]
SEQ --> LOOKUP[TutorialUIObject.GetUiObject]
LOOKUP --> TARGET[RectTransform actif]
SEQ --> HIGHLIGHT[TutorialSquareHighlight]
SEQ --> ARROW[TutorialArrow]
SEQ --> CLICK{Type d'attente}
CLICK -->|clic overlay| CATCHER[TutorialClickCatcherControl]
CLICK -->|vrai bouton| BUTTON[TutorialUIButton.clicked]
SEQ --> DONE[SetScenarioStep / SetUserState]
DONE --> STOP[Status.Stop]
STOP --> CLEAN[cleanup automatique]

Composants à connaître

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

Créer un tutoriel de zéro — procédure complète

1. Décider ce qui déclenche le tutoriel

Commencer par le trigger, pas par la classe du tutoriel. Exemples réels :

Déterminer également si le tutoriel doit être joué une seule fois. Si oui, choisir son mécanisme de persistance avant d'écrire le contenu.

2. Rendre les éléments UI trouvables par le tutoriel

Pour chaque élément à surligner ou suivre, ouvrir le prefab correspondant dans Unity et ajouter TutorialUIObject au GameObject possédant le RectTransform. Dans l'Inspector, remplir le champ uiObejctName avec un UID stable, par exemple :

create_product_button
calculator_button
cash_label
my_new_feature_button

Le tutoriel pourra ensuite récupérer la cible avec :

RectTransform target = TutorialUIObject.GetUiObject("my_new_feature_button");

Important : TutorialUIObject s'enregistre dans le dictionnaire statique uniquement dans OnEnable(). Si le GameObject/prefab/panel n'est pas actif, GetUiObject() retourne null. Il faut alors ouvrir le panel avant le lookup ou attendre jusqu'à ce que la cible existe.

Si plusieurs variantes d'UI peuvent représenter la même étape, utiliser :

string[] candidates = { "button_practice", "button_summative", "button_consultation" };
RectTransform target = TutorialUIObject.GetFirstUiObject(candidates);

Éviter les UID dupliqués sans raison : le dictionnaire garde une liste mais GetUiObject() retourne le premier élément actif. Deux objets actifs portant le même UID peuvent donc viser la mauvaise cible.

3. Si l'utilisateur doit cliquer sur la vraie cible, utiliser TutorialUIButton

Un simple TutorialUIObject permet de trouver/surligner l'objet, mais il ne sait pas si l'utilisateur l'a activé. Pour une étape qui doit attendre un vrai clic, utiliser TutorialUIButton.

Dans l'Inspector, il faut assigner explicitement l'un des champs correspondant au contrôle réel :

La classe ne fait pas automatiquement un GetComponent<Button>(). Si aucune référence n'est assignée, clicked ne passera jamais à true.

Pattern réel :

RectTransform target = TutorialUIObject.GetUiObject("my_new_feature_button");
TutorialUIButton tutoButton = target.GetComponent<TutorialUIButton>();

while (!tutoButton.clicked)
{
    ShowSquareHighlight(target);
    ShowArrow(target);
    yield return Status.Running;
}

HideArrow();
RemoveSquareHighlight();

TutorialUIButton.clicked s'auto-reset après avoir été lu comme true. Il faut donc l'utiliser comme un signal ponctuel, typiquement dans un while (!tutoButton.clicked).

4. Ajouter les textes

Les textes doivent normalement passer par le système de localisation existant. Ajouter les UID FR/EN dans la Google Sheet appropriée puis utiliser le pipeline documenté dans Textes, localisation et mise à jour.

Dans le code :

ShowCharacter(
    Game.Texts.GetText("my_tutorial_step_1"),
    TutorialCharacter.eCharacterPos.eTop,
    true
);

GenericTutorial accepte soit un UID existant, soit une chaîne déjà résolue. Il teste Game.Texts.HasText() avant de résoudre le texte.

5. Créer la classe pour un tutoriel multi-étapes

Créer une classe qui hérite de MenuTutorialSequence et surcharger Content(). Le moteur est une séquence coroutine : tant qu'une étape attend quelque chose, retourner Status.Running; à la fin, retourner Status.Stop.

using System.Collections.Generic;
using UnityEngine;

namespace UnikSim
{
    public class MyFeatureTutorial : MenuTutorialSequence
    {
        public override IEnumerator<Status> Content()
        {
            ShowCharacter(
                Game.Texts.GetText("my_feature_tuto_1"),
                TutorialCharacter.eCharacterPos.eTop,
                true
            );

            WaitForClick(0.1f);
            while (!_isClicked)
            {
                yield return Status.Running;
            }

            RectTransform target = null;
            while (target == null)
            {
                target = TutorialUIObject.GetUiObject("my_feature_button");
                yield return Status.Running;
            }

            TutorialUIButton tutoButton = target.GetComponent<TutorialUIButton>();

            ShowCharacter(
                Game.Texts.GetText("my_feature_tuto_2"),
                TutorialCharacter.eCharacterPos.eTop,
                false
            );

            while (!tutoButton.clicked)
            {
                ShowSquareHighlight(target);
                ShowArrow(target);
                yield return Status.Running;
            }

            HideArrow();
            RemoveSquareHighlight();
            RemoveCharacter();

            Game.User.SetScenarioStep(Scenario.eScenarioStep.eMyFeatureTutorial);

            yield return Status.Stop;
        }
    }
}

6. Comprendre WaitForClick : ce n'est PAS un clic sur la cible

WaitForClick() active TutorialClickCatcherControl, un overlay global. L'utilisateur clique sur l'overlay pour continuer. Ce helper est donc parfait pour une étape informative, mais pas pour exiger qu'un bouton métier soit réellement pressé.

Comportement voulu À utiliser
“Lis ce message puis clique n'importe où pour continuer” WaitForClick() + _isClicked
“Clique exactement sur ce bouton pour continuer” TutorialUIButton.clicked
“Attends quelques secondes” WaitForTime() + TimeIsReached

7. Choisir la bonne caméra

Les helpers utilisent une caméra pour convertir le RectTransform en position écran.

ShowSquareHighlight(target, Game.Instance.PopupCamera);
ShowArrow(target, Game.Instance.PopupCamera);

Un highlight décalé ou une flèche au mauvais endroit est souvent simplement une mauvaise caméra.

8. Déclencher le tutoriel

Le lancement standard est :

new MyFeatureTutorial().Start();

Pour un tutoriel one-shot :

if (!Game.User.IsScenarioStepDone(Scenario.eScenarioStep.eMyFeatureTutorial))
{
    new MyFeatureTutorial().Start();
}

Le meilleur endroit est généralement l'endroit où l'état nécessaire vient d'être atteint : un GameState, l'ouverture d'un panel, la création d'une donnée ou la réception d'un résultat. Ne pas lancer un tutoriel dans Update() sans garde : il serait recréé continuellement.

9. Ajouter un ScenarioStep si le tutoriel doit être mémorisé

Scenario.eScenarioStep est un enum [Flags]. Chaque valeur représente un bit. Les valeurs existantes utilisent 1 << n.

Pour ajouter une nouvelle étape persistante :

eMyFeatureTutorial = 1 << 18

Prendre le prochain bit libre et ne jamais réutiliser un bit existant. eFinishQuestions = 1 doit rester égal à 1 : le commentaire du code indique que le serveur dépend de cette valeur.

À la fin réelle du tutoriel :

Game.User.SetScenarioStep(Scenario.eScenarioStep.eMyFeatureTutorial);

SetScenarioStep() ajoute le bit au masque local puis, par défaut, appelle Game.Backend.SetScenarioStep() pour sauvegarder le masque côté backend.

Si le statut doit être propre à une simulation plutôt qu'au compte utilisateur global, examiner User.SetUserState(simUid, stateUid) / IsUserStateDone() au lieu d'utiliser un nouveau ScenarioStep.

10. Cas simple : ne PAS créer une nouvelle classe

Pour un message de validation ou une aide one-shot sans workflow :

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

Avec popup :

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

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

new GenericTutorial(
    "not_enough_budget_body",
    "games_budget",
    TutorialCharacter.eCharacterPos.eDown
).StartGeneric().ShowFlashing();

Ce que MenuTutorialSequence fait automatiquement

Start() arrête le tutoriel actuellement actif avant de démarrer le nouveau. Stop() nettoie character, square highlight, arrow, input blocker, click catcher et bouton Skip.

Le bouton Skip est activé par défaut pour le professeur via Game.User.IsTeacher. Certains tutoriels “force watch”, comme la création initiale de simulation, le désactivent explicitement.

Exemples du code à copier selon le besoin

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

Checklist avant de considérer le tutoriel terminé

  1. Le ou les textes existent en FR et EN.
  2. Chaque cible possède un UID TutorialUIObject stable.
  3. Chaque étape qui exige un vrai clic possède un TutorialUIButton correctement branché au Button/BasicButton/Toggle.
  4. Les cibles existent réellement au moment du lookup; sinon le code attend leur apparition.
  5. La bonne caméra est utilisée pour chaque popup/panel.
  6. Le trigger ne peut pas relancer le tutoriel en boucle.
  7. Le ScenarioStep/UserState n'est marqué terminé qu'après la vraie réussite.
  8. Skip/fermeture d'écran ne laisse ni highlight ni blocker.
  9. Le tutoriel fonctionne après logout/login.
  10. Le tutoriel fonctionne dans le build WebGL, pas uniquement dans l'Editor.

Debug — symptômes fréquents

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

Règle de conception

Un tutoriel ne devrait pas dupliquer la logique métier. Il doit observer l'UI et guider l'utilisateur. L'action réelle reste dans les contrôles/controllers existants; le tutoriel attend simplement que cette action se produise.

Build Unity, WebGL et versioning client

Build WebGL

Le build est défini dans Assets/Src/Editor/Builder.cs. Le menu Unity Uniksim → Build WebGL appelle WebGLBuild(), qui build Assets/game.unity vers le dossier Build/.

flowchart LR
CODE[Code + Prefabs + Resources] --> VER[version.txt / sub_version.txt]
VER --> MENU[Uniksim / Build WebGL]
MENU --> SCENE[Assets/game.unity]
SCENE --> BUILD[game/Build/Build/*]
BUILD --> DEPLOY[node.js/deploy/deployToCloud.bat]
DEPLOY --> SPACE[uniksim-data/version/*]
SPACE --> DB[constants table: active WebGL version]
DB --> WEB[Web app loads that version]

Version du client

Game.cs et l'UI de profil lisent ces fichiers. Assets/Src/Editor/AutoVersion.cs contient un ancien incrément automatique, mais son code est commenté : le build n'incrémente pas automatiquement version.txt.

Script de déploiement

Le déploiement du build WebGL est géré par :

deployToCloud.bat exécute node deploybuild.js. deployBuild.js lit automatiquement ../game/Assets/Resources/data/version.txt puis upload les artefacts dans uniksim-data sous un dossier portant cette version.

Fichiers uploadés

Fichier local Clé distante
game/Build/Build/Build.data.unityweb <version>/Build.data.unityweb
game/Build/Build/Build.wasm.unityweb <version>/Build.wasm.unityweb
game/Build/Build/Build.framework.js.unityweb <version>/Build.framework.js.unityweb
game/Build/Build/Build.loader.js <version>/Build.loader.js

Important : modifier version.txt avant le build et le déploiement. Le dossier distant est choisi à partir de cette valeur.

Version active dans la base de données

Après le déploiement, mettre à jour la version WebGL active dans la table constants de la base uniksim, enregistrement id = 1. Le backend la lit dans GameService.getVersion().

phpMyAdmin : https://db.uniksim.com/phpmyadmin/index.php?db=uniksim&table=constants&target=sql.php

Instance Colonne à modifier Propriété backend
alpha dev_version devVersion
alpha2 dev2_version dev2Version
demo demo_version demoVersion
demoLive demoLive_version demoLiveVersion
production / game live_version liveVersion

La valeur en DB doit correspondre exactement au contenu de version.txt qui vient d'être déployé. Sinon le frontend tentera de charger un autre dossier WebGL.

Quand rebuilder

Changement Rebuild Unity ?
C# Unity, scène, prefab, ScriptableObject Oui
JSON dans Resources à embarquer Oui
Cloud Script BrainCloud uniquement Non
Texte publié dynamiquement sur le serveur Pas nécessairement

Procédure complète

  1. Vérifier le diff Git et les assets/Resources sauvegardés.
  2. Choisir le nouveau numéro et modifier Assets/Resources/data/version.txt. Vérifier sub_version.txt si nécessaire.
  3. Lancer Uniksim → Build WebGL.
  4. Confirmer zéro erreur dans le BuildReport et vérifier les quatre artefacts dans game/Build/Build/.
  5. Depuis node.js/deploy/, lancer deployToCloud.bat.
  6. Dans phpMyAdmin, table uniksim.constants, mettre à jour la colonne de l'environnement ciblé avec la même version.
  7. Charger l'instance cible et confirmer que le nouveau build est utilisé.
  8. Tester login, chargement de simulation, ouverture du jeu et une commande BrainCloud.

Diagnostic rapide

Symptôme À vérifier
L'ancien build se charge Colonne *_version de constants.
404 sur les fichiers Unity Version DB vs dossier uploadé dans uniksim-data.
Mauvais dossier déployé version.txt avant deployToCloud.bat.
Changement Cloud Script absent Importer les Server Scripts BrainCloud; un rebuild Unity ne suffit pas.

Tests automatisés des Server Scripts

But

Les tests automatisés valident la logique métier des Cloud Scripts sans passer par l'UI Unity. Ils servent surtout à protéger les moteurs complexes By Module : création des données, bills, answer key, grading, inventaire et factories.

Emplacement

Les tests se trouvent sous node.js/backend/braincloud/tests/.

Dossier Ce qui est testé
unit/by_module_tests/answer_key_tests/ Génération des bonnes réponses, fonctions auxiliaires, petty cash.
unit/by_module_tests/bills_tests/ Création et utilitaires des bills.
unit/by_module_tests/classes_tests/ Classes métier comme l'inventaire.
unit/by_module_tests/create_module_type_data_tests/ Génération initiale du Module Type Data.
unit/by_module_tests/factories_tests/ Factories serveur, notamment BillFactory.
unit/by_module_tests/grade_module_tests/ Correction, états financiers, formulaires fiscaux et modules spécialisés.

Infrastructure de test

Quand ajouter un test

Ajouter ou modifier un test dès qu'un changement touche une fonction pure ou déterministe du moteur serveur. Les zones à protéger en priorité sont :

Stratégie

flowchart LR
CHANGE[Changement métier] --> UNIT[Test ciblé]
UNIT --> EDGE[Cas limites]
EDGE --> REG[Régression du module]
REG --> DEBUG[Debug local si le problème dépend de documents réels]

Tests vs debug local

Les tests automatisés ne remplacent pas Debug local des Server Scripts BrainCloud. Un test unitaire valide une fonction avec des fixtures contrôlées; le debug local permet d'exécuter un vrai Cloud Script avec des documents issus de MongoDB ou récupérés depuis BrainCloud.

Lancer réellement les tests

Le projet possède un runner Web, pas seulement des fichiers .test.js.

Runner Web recommandé

Depuis node.js/backend/braincloud/ :

node tests/web-runner/server.js

Le serveur écoute par défaut sur http://localhost:3000 et ouvre automatiquement le navigateur. L'UI liste les suites sous tests/unit/by_module_tests, permet de sélectionner des tests individuels et de lancer en mode normal ou debug.

Mode normal vs debug

Dépendance MongoDB locale

tests/lib/test_base.js crée MongoDBClient et s'authentifie avant d'initialiser les utils. Si les tests ne démarrent même pas, vérifier d'abord le MongoDB local et sa configuration avant de chercher une régression métier.

Point à connaître : game-specific de test

Le bootstrap de test inclut actuellement explicitement /acc2/utils/acc2_utils. Si une nouvelle suite dépend réellement d'une différence Acc1/Acc2, ne pas supposer que le runner change automatiquement de game-specific.

Créer un nouveau test

  1. Placer le fichier sous la catégorie appropriée dans tests/unit/by_module_tests/.
  2. Créer un TestSuite.
  3. Ajouter les cas avec suite.addTest(name, fn).
  4. Initialiser la fixture commune avec suite.init() si nécessaire.
  5. Utiliser les assertions de unit_test_core.js : areEqual, isTrue, inRange, etc.
  6. Ajouter les fixtures réutilisables dans tests/data ou tests/utils, pas en duplicata dans chaque test.
  7. Lancer le test seul, puis la suite complète du domaine.

Quand un test doit être ajouté

Tout changement Answer Key/Grading/Create Module Type Data/BillFactory/inventory doit avoir au minimum un test qui aurait échoué avant le changement. Un bug corrigé sans test de régression est considéré incomplet lorsque la fonction est testable localement.

Debug local des Server Scripts BrainCloud

Pourquoi ce système existe

Le dossier node.js/backend/braincloud/debugging/ recrée localement une partie de l'environnement d'exécution BrainCloud. C'est l'outil principal pour exécuter un Cloud Script du dépôt, suivre ses include(), inspecter ses variables et utiliser de vrais documents sans devoir déclencher toute la simulation depuis Unity.

Fichiers clés

FichierRôle
debugging/server_scripts_debugging_base.jsBootstrap du runtime local : crée include(), initialise MongoDB et le client BrainCloud, charge le bridge puis le script de debug.
debugging/server_scripts_debugging.jsScratchpad d'exécution : définir data, appeler un script, tester un module ou écrire une fonction de diagnostic.
debugging/utils/bridge.jsÉmule l'objet global bridge disponible dans les Cloud Scripts BrainCloud.
debugging/utils/custom_entity_service_proxy.jsÉmule l'accès aux Custom Entities en utilisant MongoDB local et BrainCloud en fallback.
utils/MongoDBClient.jsConnexion au MongoDB local localhost:27017, base uniksim.
utils/BraincloudClient.jsAccès à l'environnement BrainCloud choisi lorsque le document n'existe pas localement.

Bootstrap

sequenceDiagram
participant N as Node
participant BASE as server_scripts_debugging_base
participant M as MongoDB local
participant BC as BrainCloudClient
participant BR as bridge local
participant DBG as server_scripts_debugging
N->>BASE: exécution
BASE->>BASE: crée global.include()
BASE->>M: authenticate()
BASE->>BC: crée le client
BASE->>BR: include debugging/utils/bridge.js
BASE->>DBG: include server_scripts_debugging.js
DBG->>DBG: exécute la fonction de test choisie

Comment include() fonctionne

server_scripts_debugging_base.js expose un global.include(path). Pour un Server Script, le chemin est résolu vers /server_scripts<path>.js. Le fichier est lu avec fs.readFileSync() puis exécuté dans le même contexte Node avec vm.runInThisContext().

La liste includedScripts évite de réinclure deux fois le même script lorsque includeOnce est actif. Cela reproduit suffisamment le modèle BrainCloud pour suivre la majorité des dépendances du dépôt.

Lancer le debugger

Depuis le dossier node.js, le point d'entrée est :

node backend/braincloud/debugging/server_scripts_debugging_base.js

Le bootstrap change lui-même le working directory vers backend/braincloud.

Préparer server_scripts_debugging.js

Le fichier est volontairement un scratchpad. La méthode la plus simple est :

  1. choisir serverInstanceType (par défaut dev);
  2. définir l'objet global data exactement comme BrainCloud le recevrait;
  3. appeler include("/_command/...") ou un script game-specific;
  4. inspecter response, les variables ou les documents produits.
data = {
  commandUid: "get_module_grade",
  commandData: {
    teamUid: "...",
    actModuleUid: "...",
    moduleType: "practice",
    actUid: "..."
  }
};

include("/acc1/command/acc1_custom_command");

testScript()

Le helper testScript({ scriptName, scriptData }) copie scriptData dans data puis appelle directement include("/" + scriptName). C'est utile lorsqu'on veut reproduire une requête Cloud Script presque telle qu'elle apparaît dans les logs.

Tester le moteur By Module sans document existant

testGeneralFunctionnalities(moduleUid) est particulièrement important. Il reconstruit le pipeline complet :

flowchart LR
SET[Static settings] --> CREATE[Create Module Type Data]
CREATE --> AK[Answer Key]
AK --> GRADE[Grade Module]
GRADE --> RE[Reassemble Answers]
RE --> OUT[Scores / Errors]

Il crée un seed, génère les données, prend les bonnes décisions, grade le résultat puis réassemble les scores. testAllModuleGeneralFunctionnalities() applique ce flux à tous les modules configurés et permet de trouver rapidement une régression globale.

Accès aux documents : MongoDB d'abord, BrainCloud ensuite

CustomEntityServiceProxy cherche d'abord le document dans le MongoDB local. Si rien n'est trouvé, il authentifie le BraincloudClient dans serverInstanceType et récupère le document distant. Les lectures suivantes peuvent donc être déboguées localement tout en utilisant un vrai document de Dev.

flowchart TD
GET[getDocumentData] --> LOCAL{Document dans MongoDB local ?}
LOCAL -->|oui| USE[Utiliser document local]
LOCAL -->|non| BC[Lire depuis BrainCloud]
BC --> USE
USE --> SCRIPT[Exécuter le Server Script]
SCRIPT --> SAVE[Écritures via proxy local]

Limites du bridge local

Le bridge n'émule pas tout BrainCloud. Plusieurs proxies retournent volontairement 501 ou ne sont pas implémentés :

Si un Server Script dépend fortement d'une de ces APIs, le debug local peut nécessiter d'étendre le proxy ou de valider la dernière étape directement sur BrainCloud.

Cas d'usage recommandés

Important avant de modifier

Ne pas laisser un appel de test accidentel actif en haut de server_scripts_debugging.js. Le fichier exécute directement le code présent lorsqu'il est inclus par le bootstrap. Garder une seule fonction de test explicitement appelée pendant la session de debug.

Outils BrainCloud et import/export des scripts

But

Cette page documente les outils qui synchronisent les Cloud Scripts entre le dépôt et les applications BrainCloud. Ils sont séparés du debugger local : ici on parle de déploiement/synchronisation des scripts, pas d'exécution métier.

Fichiers principaux

api_builder_utils.js

Ce script enveloppe le CLI bc-builder. Il peut récupérer la liste des scripts, lire un script distant, comparer les versions et importer/exporter selon l'application sélectionnée.

Les environnements gérés explicitement incluent Dev, Dev2 et Demo. Le script permet aussi des options de remplacement forcé selon le workflow choisi.

Flux d'import

flowchart LR
LOCAL[server_scripts/] --> UTIL[api_builder_utils.js]
UTIL --> CLI[bc-builder]
CLI --> APP[BrainCloud Dev / Dev2 / Demo]

Flux d'export

flowchart LR
APP[BrainCloud] --> CLI[bc-builder]
CLI --> UTIL[api_builder_utils.js]
UTIL --> LOCAL[server_scripts/]

Avant un import

  1. Vérifier l'environnement ciblé.
  2. Vérifier le diff Git des Server Scripts.
  3. Vérifier si des fichiers ont aussi changé directement dans BrainCloud.
  4. Éviter un force override global sans comprendre les différences.
  5. Après import, exécuter une commande représentative ou le debugger local avec la même logique.

Relation avec les autres outils

Besoin Outil
Valider une fonction métier isolée Tests automatisés
Exécuter un Cloud Script avec de vraies données Debug local des Server Scripts
Envoyer/récupérer les scripts BrainCloud api_builder / import-export scripts
Importer les static data Google Sheets node.js/exports/

Commandes exactes

Importer les Cloud Scripts depuis BrainCloud vers le dépôt

cd node.js/backend/braincloud
import_server_scripts.bat
# équivalent :
node api_builder_utils.js import_scripts

Exporter/publier les Cloud Scripts vers BrainCloud

cd node.js/backend/braincloud
export_server_scripts.bat
# équivalent :
node api_builder_utils.js export_scripts

api_builder_utils.js reconnaît aussi les cibles Demo/Dev2 selon les arguments du workflow. Vérifier le prompt/log de destination avant toute publication.

Copier une simulation entre environnements

exports/data_importer_braincloud.js est un outil distinct de la static data. Il copie une simulation runtime et les documents associés.

cd node.js
node exports/data_importer_braincloud.js copySim <simUid> <source> <destination>

Sources autorisées : live, dev, dev2, demo. Destinations : dev, dev2, demo. Le script refuse explicitement une destination live.

Documents copiés

Le script crée/authentifie aussi les users nécessaires dans la destination et maintient les mappings de profils copiés.

Sync vs async

Le script a deux chemins : copyActs() pour le flow classique et copyAllActsAsync()/copyAsyncTeam() pour les simulations async, où certains UIDs d'activity sont construits par équipe.

Copier une activité vers MongoDB pour debug

api_builder_utils.js expose aussi copyAct [actUid] [backEndType]. C'est utile pour préparer des données réelles dans le MongoDB utilisé par le debugger local.

Checklist opérationnelle

  1. Confirmer source et destination.
  2. Ne jamais recopier de credentials du script dans la documentation/log public.
  3. Copier.
  4. Vérifier simulation + teams + activity + results dans la destination.
  5. Tester login avec un utilisateur copié.
  6. Pour By Module, vérifier également les teamsModules.
  7. Si un nouveau entity type est ajouté au produit, vérifier si cet outil doit le copier.

Scénarios runtime — configuration, réponses et évaluation

Rôle du système Scenario

Le système Scenario gère le contenu d'introduction/contexte, les questions, les réponses de segments et leur évaluation. Il ne doit pas être confondu avec :

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

Fichier Responsabilité
Scenario.cs Réponses booléennes et réponses de segments de l'étudiant.
ScenarioAnswer.cs Réponse attendue par UID.
ScenarioEval.cs Scenario + nombre de bonnes réponses scénario/segments.
ScenarioPages.cs ScriptableObject qui décrit les pages affichées.
ScenarioPanelControlBase.cs Moteur UI principal, navigation, validation et sauvegarde.
ScenarioSegGridControlBase.cs Base de la grille de segments.
EvalCorrectedPopupControl.cs Vue de correction.

ScenarioPages

Chaque PagesDefinition contient :

Chargement By Year

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

Ce chargement est fait dans StaticDataByYearController.Initialize().

Chargement By Module

string moduleUid = moduleByUid.Key.SnakeCaseToPascalCase();

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

StaticDataByModuleController charge donc un asset ScenarioPages distinct par module lorsqu'il existe.

Créer ou modifier un Scenario de A à Z

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

Les réponses des questions sont stockées sous des UIDs comme question_0, question_1, etc. Vérifier que questionsStartIndex et numQuestions correspondent aux réponses statiques attendues.

2. Modifier le bon ScenarioPages asset

ScenarioPagesEditor.cs masque la référence de segment grid lorsque useSegmentsInScenario est false.

3. Ajouter les textes

Ajouter les titres, paragraphes, questions et choix via le pipeline de localisation. Ne pas hardcoder le contenu dans ScenarioPanelControlBase.

4. Ajouter un composant game-specific si nécessaire

Le socle Scenario est commun, mais certains jeux possèdent des rendus propres :

Un nouveau rendu métier reste sous Assets/GameSpecific/<gameUid>/Src/UI.

5. Vérifier InitRuntimePages()

ScenarioPanelControlBase.InitRuntimePages() construit les pages runtime. En By Year, il peut insérer les segments dans la séquence lorsque useSegmentsInScenario est activé.

6. Sauvegarder les réponses de questions

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 les clés reçues dans evaluation.scenario.answers, puis sauvegarde le document evaluation.

7. Sauvegarder les segments

SaveEvalSegment appelle _command/save_eval_segment. Le script :

  1. écrit le segment dans evaluation.scenario[segmentUid];
  2. sauvegarde evaluation;
  3. envoie au professeur un message get_data qui devient update_eval.

8. Calculer les scores

Le client appelle les commandes game-specific :

Elles délèguent au moteur Eval<Game>, sauvegardent dans evaluation.scores, puis retournent les informations utilisées par ScenarioEval.

9. Marquer la fin

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

eFinishQuestions doit rester la valeur 1. Le commentaire du code indique que le serveur suppose explicitement cette valeur.

10. Tester la correction

Tester GetScenarioEvaluation, ScenarioEval.Deserialize(), la vue corrigée et la vue professeur. Ajouter une question sans corriger ce chemin laisse la feature incomplète.

ScenarioStep — progression persistée

Scenario.eScenarioStep est un enum de flags stocké sous forme de bitmask. Il suit la progression utilisateur, pas les réponses de Scenario.

Exemples :

Ajouter un ScenarioStep

  1. Ajouter un nouveau bit libre avec 1 << n.
  2. Ne jamais renuméroter un ancien bit déjà persisté.
  3. Tester IsScenarioStepDone() avant le flow concerné.
  4. À la fin, appeler SetScenarioStep().
  5. Si la séquence peut être rejouée volontairement, décider si UnSetScenarioStep() est nécessaire.

User.SetScenarioStep() OR le nouveau bit dans le masque local puis appelle _command/set_scenario_step, qui écrit le masque complet dans userDetails.scenarioStep.

States Scenario

Scenario réouvert après le début

ScenarioPopupControl permet de consulter le scénario après le flow initial. Il conserve la dernière page consultée et réutilise ScenarioPanelControl. En By Module, ScenarioWindowControl adapte les titres selon le module et ses settings.

Debug Scenario

Symptôme Vérifier
Page absente Nom/path de ScenarioPages, pagesDefinitions, asset dans Resources.
Question visible mais non persistée question_N → SaveScenarioAnswers → evaluation.scenario.answers.
Segment non mis à jour chez le prof save_eval_segment → get_data/update_eval.
Score faux Good answers static data + Eval<Game>.
Étudiant bloqué après validation userDetails.scenarioStep et bit eFinishQuestions.
Correction vide get_scenario_evaluation, ScenarioEval, corrected popup.

Checklist avant merge

Timelines Unity — architecture et création de séquences

Rôle des Timelines Unity

Les Timelines sont des séquences audiovisuelles Unity basées sur PlayableDirector. Elles servent à présenter une animation de résultats, de première ouverture ou de lancement produit tout en bloquant temporairement l'interaction normale avec l'UI.

Elles sont différentes des Tutoriels : un tutoriel guide activement l'utilisateur dans l'interface et attend souvent ses clics; une Timeline joue une séquence Unity et appelle un callback lorsqu'elle se termine.

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

Le socle est Assets/Src/Game/Timeline/TimelineBase.cs.

TimelineBase gère :

Ce que Play() fait réellement

  1. active scene3dRoot;
  2. désactive la caméra UI;
  3. active la caméra offscreen;
  4. redirige le Canvas vers cette caméra;
  5. copie le viewport de la caméra UI;
  6. calcule le scale horizontal entre les valeurs 4:3 et 16:9;
  7. bloque les raycasts UI;
  8. lance le PlayableDirector;
  9. s'abonne à director.stopped.

Lorsque le director s'arrête, OnPlayFinish() se désabonne, appelle Stop(), puis déclenche le callback.

Timelines communes existantes

Classe Responsabilité
CreateProductTimeline Séquence générique après création/lancement d'un premier objet métier.
YearResultsTimeline Animation de résultats annuelle By Year avec graph, profit/perte et anti-bankruptcy.
FirstModuleTimeline Première ouverture d'un module By Module.
ModuleResultsTimeline Animation de correction/résultats d'un module summative.

Timelines game-specific

Les jeux peuvent spécialiser le socle :

Une Timeline qui dépend d'une structure game-specific doit rester dans le dossier du jeu, pas dans le socle commun.

Assets .playable

Une Timeline Unity est composée de deux parties :

Changer uniquement le C# sans vérifier l'asset peut laisser des bindings cassés. Changer uniquement le .playable nécessite aussi de vérifier que le prefab/scene fournit toutes les références utilisées par les tracks.

Créer une Timeline de zéro — procédure complète

1. Vérifier qu'une Timeline est le bon outil

Utiliser une Timeline si le besoin est principalement une séquence visuelle/animée. Pour guider un utilisateur vers un bouton, attendre un clic réel ou surligner une zone UI, utiliser le système Tutoriels.

2. Créer l'asset Timeline

Dans Unity :

  1. créer un asset Timeline .playable dans le dossier Resources approprié;
  2. ajouter les tracks nécessaires;
  3. configurer clips, animations et bindings;
  4. tester la séquence directement dans l'éditeur.

3. Créer la classe C#

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 les références du prefab/scene

Le composant hérité de TimelineBase doit avoir des références valides vers :

5. Choisir le bon point de déclenchement

Exemples réels :

6. Préparer les données avant Play()

Une Timeline ne devrait pas aller chercher arbitrairement toutes ses données elle-même. Le trigger prépare les valeurs nécessaires, puis les passe à la Timeline.

Exemple YearResultsTimeline :

7. Gérer la fin dans onFinish

Le code qui ouvre l'écran suivant ou change la progression doit être exécuté dans le callback.

MyTimeline.Instance.PlaySequence(
    message,
    () =>
    {
        OpenNextPanel();
    }
);

Ne pas écrire :

MyTimeline.Instance.PlaySequence(message, null);
OpenNextPanel(); // s'exécute immédiatement

Empêcher une Timeline de rejouer

TimelineBase ne persiste aucun état par lui-même. Le mécanisme anti-replay dépend du contexte.

Cas Mécanisme
Première ouverture de module Scenario.eScenarioStep.eStudentFirstModule.
Résultats annuels SetYearSequenceSeen(yearId).
Résultats d'un module UserState.ModuleUserState.STUDENT_RESULTS.
Premier produit / premier launch Condition métier + éventuellement ScenarioStep.

FirstModuleTimeline

FirstModuleTimeline.PlayFirstModule() :

  1. injecte le texte;
  2. joue firstModuleDirector;
  3. à la fin, sauvegarde eStudentFirstModule;
  4. appelle le callback qui ouvre le module.

Le trigger est présent dans les contrôles By Module lorsque l'étudiant n'a jamais vu cette séquence.

ModuleResultsTimeline

Cette Timeline est plus complexe car les données peuvent ne pas être encore disponibles lorsqu'on veut la jouer.

Attente des résultats

Si TryGetTeamByModuleResult() ne trouve pas encore les résultats :

  1. la Timeline s'abonne à onNewTeamModuleResults;
  2. attend le bon actModuleUid / module type;
  3. se désabonne dès réception;
  4. prépare puis joue la séquence.

Ce pattern est à réutiliser lorsqu'une nouvelle Timeline dépend d'une donnée asynchrone : abonnement temporaire + filtre strict + désabonnement immédiat.

Choix du director

Le score moyen du module choisit entre :

Pause et report

ChangePlayableSpeed(false) met le root playable à vitesse 0 et affiche les options. La Timeline peut alors montrer les notes via reportControl.ShowGrades() avant de continuer.

YearResultsTimeline

PlayNewResults() :

  1. récupère le RankingsData de l'année;
  2. met à jour le whiteboard/graph;
  3. compare la valeur actuelle à l'année précédente ou à GraphFirstValue;
  4. détermine profit/perte;
  5. choisit le director;
  6. construit le texte localisé;
  7. marque la séquence annuelle comme vue via SetEndOfYearSequenceSeen(yearId).

Créer une Timeline avec plusieurs variantes

Pour un même événement avec plusieurs résultats visuels, garder une seule classe et plusieurs PlayableDirector lorsque les inputs/lifecycle sont identiques.

PlayableDirector director =
    score >= threshold
        ? goodDirector
        : badDirector;

Play(onFinish, director);

Créer plusieurs classes uniquement si les séquences ont des données et un lifecycle réellement différents.

Interaction avec les caméras et l'UI

TimelineBase.Play() désactive la caméra UI et empêche les raycasts. Un bug pendant la séquence peut donc laisser toute l'application visuellement ou fonctionnellement bloquée si Stop() n'est jamais exécuté.

Stop()

Stop() doit toujours rétablir :

Gestion du ratio d'écran

TimelineBase interpole le scale horizontal entre scale_4_3 et scale_16_9 selon Screen.width / Screen.height, puis clamp le résultat.

Si une séquence est correctement cadrée en 16:9 mais coupée sur un autre ratio, vérifier ces deux valeurs sur le prefab avant de modifier la caméra ou les clips.

Timelines game-specific

MK1

TimelinesMk1 hérite de YearResultsTimeline et ajoute un launchProductDirector ainsi qu'un whiteboard MK1.

ENT1

LaunchProductTimeline gère un whiteboard de lancement produit et ses propres directors de résultats.

Build et assets

Modifier :

nécessite un nouveau build Unity/WebGL.

Debug Timeline

Symptôme À vérifier
Écran noir après la séquence director.stopped a-t-il été déclenché ? Stop() a-t-il tourné ?
UI visible mais non cliquable UICanvasGroup.blocksRaycasts.
Timeline rejouée à chaque ouverture Mécanisme anti-replay non persisté.
Callback plusieurs fois Abonnement director.stopped ou event résultats non désabonné.
Mauvaise variante good/okay/bad Valeur métier passée à la Timeline et seuils de sélection.
Animation avec anciennes données Le trigger passe-t-il les données actuelles avant Play() ?
Résultats module jamais affichés onNewTeamModuleResults, filtres actModuleUid/moduleType.
Layout déformé scale_4_3, scale_16_9, screenTransform.
Playable ne joue pas Director / asset .playable / bindings Unity.

Checklist avant merge

Les jeux UnikSim — rôle, contenu et navigation de la documentation

Pourquoi cette page existe

Cette page est une carte fonctionnelle du produit. Elle explique ce que chaque jeu simule, ce que l'étudiant y fait et quelles pages de documentation ouvrir en premier. Elle ne remplace pas les pages d'architecture détaillées.

Les deux grandes familles

flowchart TD
GAME[UnikSim Game] --> YEAR[By Year]
GAME --> MODULE[By Module]
YEAR --> ENT1
YEAR --> HR1
YEAR --> MGT1
YEAR --> MK1
YEAR --> MK2
MODULE --> ACC1
MODULE --> ACC2
Famille Logique principale Jeux
By Year Les équipes prennent des décisions pendant une année; le moteur génère les résultats; l'état est transporté vers l'année suivante. ENT1, HR1, MGT1, MK1, MK2.
By Module L'activité est composée de modules configurables avec pages data-driven, Answer Key, Grading et résultats de module. ACC1, ACC2.

ENT1 — entreprise / gestion intégrée

Contexte : équipe de direction d'une entreprise qui produit des téléphones intelligents.

Objectif fonctionnel : fabriquer et commercialiser des téléphones adaptés aux consommateurs tout en contrôlant les coûts de production.

Ce que le jeu combine

Où regarder d'abord

Repère mental : ENT1 est le jeu By Year le plus transversal : une décision peut traverser marketing → production → RH → finance → ventes.

MK1 — marketing produit

Contexte : entreprise de vêtements.

Objectif fonctionnel : faire prospérer l'entreprise en commercialisant des produits adaptés aux consommateurs.

Concepts importants

Où regarder d'abord

Repère mental : MK1 est centré sur le mix marketing et la performance comparative des produits/lancements.

MK2 — marketing avancé / international

Contexte : entreprise qui produit des skis.

Objectif fonctionnel : commercialiser des produits adaptés aux consommateurs et faire croître l'entreprise.

Ce qui le distingue de MK1

Où regarder d'abord

Repère mental : ne pas traiter MK2 comme un simple “MK1 avec d'autres textes”; ses pays, relaunchs, stocks, loyalty et notoriety changent le moteur.

HR1 — ressources humaines

Contexte : département RH d'une entreprise d'organisation et de production de spectacles musicaux.

Objectif fonctionnel : contrôler les dépenses RH tout en permettant à l'entreprise de fonctionner et de maximiser ses résultats.

Concepts importants

Où regarder d'abord

Repère mental : beaucoup d'actions HR1 modifient immédiatement le cash avant même la génération annuelle.

MGT1 — processus de gestion (DEPRECATED)

Contexte : équipe de cadres d'une entreprise de création de jeux vidéo.

Objectif fonctionnel : appliquer le processus de gestion — planification, organisation, direction et contrôle — afin d'utiliser correctement les ressources financières.

Concepts importants

Où regarder d'abord

Repère mental : MGT1 est fortement structuré autour des employés → temps de travail → features → résultat de l'organisation.

ACC1 — Accounting / entreprise de piscines

Contexte : département de comptabilité d'une entreprise spécialisée dans la vente de piscines.

Objectif fonctionnel : compléter le cycle comptable à travers des modules pédagogiques.

Concepts importants

Où regarder d'abord

ACC2 — Accounting / café et machines à café

Contexte : département de comptabilité d'une entreprise spécialisée dans la vente de café et de machines à café.

Objectif fonctionnel : comme ACC1, compléter le cycle comptable à travers des modules By Module.

Différence de haut niveau

ACC2 réutilise le même moteur comptable qu'ACC1, mais son univers de companies/partners/products/services diffère et il contient notamment du contenu Petite caisse.

Où regarder d'abord

Pages Commun à connaître peu importe le jeu

Si tu touches... Lire...
Login / navigation / changement d'état Machine d'état client.
Nouvelle commande serveur Backend Unity ↔ BrainCloud.
Custom Entity / persistance Documents et persistance BrainCloud.
Synchronisation live RTT, messages et synchronisation.
Static data Données statiques et import/export.
Textes Textes, localisation et mise à jour.
Tutoriel Tutoriels — création de A à Z.
Scenario Scénarios runtime.
Animation Timeline Timelines Unity.
Build / déploiement Build Unity, WebGL et versioning client.
Test serveur Tests automatisés des Server Scripts.
Debug serveur Debug local des Server Scripts BrainCloud.

Comment choisir où chercher quand un bug arrive

  1. Le bug existe dans plusieurs jeux ? Commencer par Commun ou le framework By Year/By Module.
  2. Il existe dans ACC1 et ACC2 ? Commencer par By Module / acc_base, pas les dossiers game-specific.
  3. Il existe seulement dans un GameUid ? Regarder static data, GameSpecific Unity et utils serveur du jeu.
  4. La donnée est bonne après reload mais pas live ? Regarder RTT/controller.
  5. La donnée est fausse même après reload ? Regarder commande/persistance/génération.
  6. Le calcul serveur est bon mais l'écran faux ? Regarder modèle Unity/UI.

Structure documentaire recommandée pour naviguer

Pour une feature inconnue, lire dans cet ordre :

  1. cette page pour identifier le jeu et le domaine;
  2. la page d'architecture de la famille (By Year ou By Module);
  3. la page game-specific si By Year, ou la page domaine Accounting si ACC;
  4. la page workflow spécialisée : bills, contextes, Answer Key, grading, RTT, tutoriels, Scenario, Timeline, build, etc.

Logbook — sauvegarde, historique et évaluation

Structure des données

Le logbook est stocké par équipe dans teamsLogbooks, avec teamUid comme UID du document. Chaque activité possède deux rubriques : student et teacher. Chaque rubrique contient une entrée courante par yearId et un historique de versions.

logbook[actUid][role][yearId] = { text, date }
logbookHistory[actUid][role][yearId] = [{ text, date }, ...]

Côté Unity, Logbook.logbookRoles indexe les activités et les rôles. LogbookRole.entries contient les entrées courantes; historyEntries contient les listes de versions. LogEntry porte le texte et une date en millisecondes depuis l'époque, convertie en heure locale pour l'affichage.

Chargement et cache

TeamBaseController.Logbook expose le modèle chargé. LoadLogbook() appelle BackEndBrainCloud.GetLogbook(), puis _command/get_logbook.js, qui retourne le document teamsLogbooks.

Le controller conserve un cache. Des demandes concurrentes de chargement sont regroupées par callbacks et _logbookIsLoading. Si le cache existe, le callback est appelé sans relire le serveur.

Édition et sauvegarde

LogbookRoleControl gère l'édition, la sauvegarde, l'annulation, le collage, Undo/Redo et la sélection d'une version. Le bouton Save applique Trim() au texte et évite l'envoi si le texte est identique à l'entrée actuelle.

L'accès étudiant à l'édition dépend du rôle : avec HasLogbookDir, le directeur du logbook est requis; sinon, en équipe, le rôle temporaire logbook_editor sert à coordonner l'édition. Il peut être repris après confirmation et est libéré à la sortie du mode édition.

  1. TeamBaseController.SaveLogbookEntry() normalise actUid en asynchrone via UtilsBase.GetActUid().
  2. Le rôle envoyé au serveur est déterminé par l'utilisateur : professeur → teacher, étudiant → student.
  3. BackEndBrainCloud.SaveLogbook() envoie teamUid, actUid, yearId, date, text et role à _command/save_logbook.
  4. Le controller met aussi à jour son entrée locale immédiatement. Cette mise à jour locale ne constitue pas un accusé de sauvegarde serveur.
  5. Le script persiste les champs de teamsLogbooks, puis envoie logbook_changed à l'équipe et au professeur.

Historique : 10 versions précédentes

save_logbook.js copie l'entrée précédente dans l'historique si son texte ou sa date diffère de la nouvelle entrée. La version précédente est insérée en tête. MAX_LOGBOOK_HISTORY = 10 limite la liste; la version la plus ancienne est retirée au-delà de cette limite. La première sauvegarde n'a pas d'entrée précédente à archiver.

Les piles Undo/Redo du champ d'édition sont locales et distinctes de cet historique persistant.

Restauration

La liste déroulante présente un choix initial, puis les versions historiques. Sélectionner une version ouvre HistoryPreviewPopupControl; la confirmation appelle RestoreLogbookHistory() avec l'index de version, après normalisation de l'activité en asynchrone.

restore_logbook_history.js retire la version choisie de l'historique, place l'entrée courante en tête, puis remplace l'entrée courante par le texte et la date d'origine de la version restaurée. Il sauvegarde le document et émet logbook_changed.

Synchronisation entre clients

Le message contient teamUid, actUid, yearId et role. Le texte complet et l'historique sont relus depuis le serveur.

TeamBaseController.OnLogbookChanged() ignore les autres équipes et les logbooks qui ne sont pas encore chargés. Pour un logbook chargé de la bonne équipe, il invalide le cache, appelle LoadLogbook(), puis déclenche onLogbookChanged(role). Le chargement initial reste donc nécessaire.

Évaluation distincte du texte

save_logbook_eval.js enregistre la note dans le document evaluation de chaque étudiant concerné. simulation.logbookPonderationType choisit un mode annuel ou global, en équipe ou individuel. En équipe, la note est appliquée aux membres; en individuel, elle vise studentUid.

getLogbookScore(), dans utils_eval.js, calcule en annuel la moyenne arrondie des notes présentes jusqu'à currentYear. Sans note, il retourne 0 si l'année est terminée, sinon -1. En global, il retourne la note stockée.

La sauvegarde des notes émet logbook_eval_changed_stub et, pour les notes annuelles, logbook_eval_by_year_changed. get_logbook_evaluation.js charge les notes annuelles d'un étudiant. Sauvegarder le texte ne sauvegarde pas sa note.

Réouverture annuelle

utils_reopen_year.js appelle deleteYearLogbook(actData, teamUid) après avoir ajusté l'état et, selon le cas, l'année courante. Le helper cible précisément yearId = actData.currentYear + 1.

Il supprime les notes de cette année pour les pondérations annuelles, puis les entrées courantes et leurs historiques pour les rôles concernés. Il sauvegarde les documents et émet les messages de changement du logbook et d'évaluation. Les notes globales ne sont pas supprimées par ce traitement annuel.

Fichiers de référence

Dans master/game/Assets/Src : Game/Data/Logbook.cs, Game/Controllers/TeamBaseController.cs, Game/BackEnd/BackEndBrainCloud.cs, UI/SimByYear/LogbookRoleControl.cs.

Dans master/node.js/backend/braincloud/server_scripts : _command/get_logbook.js, save_logbook.js, restore_logbook_history.js, save_logbook_eval.js, get_logbook_evaluation.js, _utils/utils_eval.js, utils_by_year.js et _utils/command/utils_reopen_year.js.

Vérifié le 30 septembre 2026 dans le code Game et BrainCloud de la branche refactor_notifications. Les fichiers de logbook et d’évaluation décrits ici sont identiques à ceux de master pour les chemins inspectés. Les parcours décrits ont été contrôlés par lecture du code; aucune sauvegarde ou réouverture de simulation réelle n'a été exécutée pour cette vérification.