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.

Revision #2
Created 2026-09-18 19:26:52 UTC by Mathis
Updated 2026-09-18 20:25:05 UTC by Mathis