Skip to main content

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 :

    PoolsManager depuis une référence sérialisée; Scheduler = new Scheduler(); MessagesDispatcher = new MessagesDispatcher(); FxMgr, CommandManager, Toaster; NotificationManager; TextsManager; la machine d’état via ContextScheduledMachine<GameStateDTO>.

    Game.Update() appelle Scheduler.Update(); Game.LateUpdate() appelle Scheduler.LateUpdate(). Un service enregistré au Scheduler n’a donc pas besoin de son propre MonoBehaviour.Update().

    Ajouter un service runtime global de A à Z

    Ne rendre un service global que s’il doit réellement exister une seule fois pendant toute la session. Un service propre à une simulation, équipe ou popup doit rester dans son controller/contexte.

    1. Choisir MonoBehaviour ou classe pure

      classe pure : pas besoin de scene lifecycle, inspector ou coroutine; MonoBehaviour : dépend de références Unity sérialisées, coroutines, callbacks Unity ou scene objects.

      2. Ajouter la propriété dans Game seulement si elle est globale

      public static MyService MyService { get; private set; }

      Pour une classe pure, l’instancier dans Game.Awake(). Pour un MonoBehaviour, injecter une référence sérialisée depuis la scène/prefab puis assigner la propriété.

      3. Définir explicitement son lifecycle

      Décider :

        quand il est initialisé; s’il dépend de Backend ou de CurrentGameUid; ce qui doit être reset lors d’un changement de simulation; ce qui doit être nettoyé dans OnDestroy() ou logout.

        4. Ne pas créer un deuxième singleton caché

        Si le service est déjà accessible via Game, éviter un second static Instance sauf raison Unity spécifique. Les doubles singletons rendent les tests et changements de scène difficiles à tracer.

        5. Tester relog / changement de jeu

        Un service global qui garde une référence au vieux TeamController, à une ancienne simulation ou à un prefab détruit produit des bugs fantômes. Tester au minimum : login → simulation A → logout/reload → simulation B.

        Scheduler — ordonnancement par priorité

        Les priorités sont exécutées dans cet ordre :

        BeginFrame
        ServerEvent
        Fsm
        World
        Render
        Ui
        EndFrame

        Le même ordre existe pour le registry de LateUpdate.

        Ajouter un callback planifié

        Game.Scheduler.Register(
            Scheduler.Priority.Ui,
            UpdateMyControl
        );
        
        // nettoyage obligatoire
        Game.Scheduler.Unregister(
            Scheduler.Priority.Ui,
            UpdateMyControl
        );

        Scheduler.Unregister() sait gérer une désinscription pendant qu’une priorité est en cours d’itération : le callback est placé dans _toRemove puis retiré après la boucle. Il faut néanmoins utiliser exactement le même delegate lors du unregister.

        ScheduledMachine

        ScheduledMachine.Start() s’enregistre automatiquement dans la priorité Fsm pour Update et LateUpdate. Stop() se désenregistre puis déclenche onStop. C’est la raison pour laquelle la machine d’état principale n’a pas son propre MonoBehaviour.Update().

        Coroutiner — lancer une coroutine hors MonoBehaviour

        Game.Instance.coroutiner sert de point d’ancrage lorsque le code appelant n’est pas lui-même un MonoBehaviour :

        Coroutine c = Game.Instance.coroutiner.StartLocalCoroutine(
            MyRoutine()
        );
        
        // si nécessaire
        Game.Instance.coroutiner.StopLocalCoroutine(c);

        Ne pas laisser une coroutine continuer après destruction logique de son owner. Si elle modifie un popup/controller qui peut disparaître, conserver la référence et l’arrêter au teardown.

        ObjectPool et PoolsManager

        ObjectPool<T> possède deux modes :

          local pool — défaut actuel : le template et les instances restent gérés par le pool local; global PoolsManager — isLocalPool = false : le template est enregistré par son Type dans Game.PoolsManager.

          Créer un objet poolable

          public class MyRow : PoolableObject
          {
              public override void Alloc()
              {
                  // remettre l’état nécessaire pour une nouvelle utilisation
              }
          
              public override void Free()
              {
                  // retirer listeners, vider données temporaires, arrêter tweens
              }
          }

          Utilisation

          _pool = new ObjectPool<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

            Le besoin est-il vraiment global ? Peut-il vivre dans un controller existant ? Le lifecycle de cleanup est-il défini ? Scheduler plutôt qu’un Update supplémentaire ? Coroutine arrêtée si owner détruit ? Pool : Alloc()/Free() symétriques ? Event local ou RTT correctement choisi ? Helper placé dans le bon domaine plutôt que dans Utils.cs ? Aucune dépendance GameSparks introduite dans une nouvelle feature BrainCloud ? Relog/changement de simulation testé ?

            Debug rapide

            Symptôme Zone probable Callback exécuté deux fois après navigation listener/Scheduler non désinscrit ou pool Free() incomplet. Objet réutilisé avec anciennes données PoolableObject.Free/Alloc. FSM cesse d’avancer ScheduledMachine stoppée/désinscrite ou exception dans callback Fsm. UI locale ne bouge pas Doozy event / Action locale. Autre client ne bouge pas Ce n’est pas GameEventMessageUtils; suivre RTT/persistance. Heure différente du serveur GlobalTime vs Game.currentServerTime. Exception “Cannot use gamesparks” Nouvelle logique branchée par erreur sur CommandManager.