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 :
PoolsManagerdepuis 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
Backendou deCurrentGameUid; - 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 sonTypedansGame.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. |