Infrastructure runtime et utils Unity
Dossier
Assets/Src/Utils/ contient l'infrastructure Unity qui ne relève pas directement d'un domaine métier.
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
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
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 :
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 :
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
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
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
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
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.