Lorsque j’ai eu à réaliser un projet de jeu vidéo cette année, j’ai trouvé un système qui me semble pour l’instant assez performant pour organiser toutes les relations entre toutes les actions du jeu.

Il s’agit d’un gestionnaire d’évènements en cascade qui permet de simplifier énormément la modélisation du projet.

Problématique initiale

La réalisation d’un jeu vidéo (comme tout projet) nécessite de disposer d’un code facilement maintenable et très maniable afin de pouvoir implémenter de nouvelles fonctions lorsque l’on ajoute du contenu au jeu. Il serait impensable de recoder une bonne partie du moteur de jeu pour ajouter un simple objectif !

Dans la plupart des projets que j’ai pu voir (certains sont d’ailleurs très bien codés!) on assiste à de beaux exemples de programmation spaghetti où toutes les fonctionnalités sont mélangées, et où l’ajout d’une nouvelle bricole demande un travail considérable.

En soit, les problèmes commencent dés lors que toutes les parties du projet doivent être rassemblées : moteur graphique, son, …

Prenons l’exemple d’une mini-map affiché dans un coin de l’écran. Un développeur débutant qui voudrait afficher un icône sur la carte lorsqu’un coéquipier meurt se rendrait dans la fonction appelée lorsqu’un coéquipier meurt et lancerait un appel vers une autre procédure qui afficherait l’icône sur la carte. Et pour moi, c’est commencer à emmêler les spaghettis.

Solution

Un système d’évènement permettrait dans l’exemple du dessus de propager à travers tous le programme un évènement lorsque le coéquipier meurt. Le module mini-map peut alors simplement attendre que cet évènement se déclenche et afficher l’icône au moment adéquat. Le code n’a absolument pas bougé en dehors du module mini-map ce qui est à mon gout plus joli.

Au lieu de devoir se casser la tête sur de la modélisation, il suffit de réaliser des schémas d’enchaînement cause->effet->cause, ce qui est largement plus naturel que des schémas d’abstraction.

Fonctionnement

Tout est basé sur trois classes : Effect, Event, Watcher

Un événement selon ma conception objet est défini ainsi :

  • Un événement possède une chaîne de caractère caractérisant son type.
  • Un événement contient des données.

Un effet :

  • Un effet appelle une méthode lorsqu’il se produit et passe en paramètre les données contenu dans l’événement l’ayant déclenché.
  • Cette méthode peut éventuellement déclencher d’autres événements.

Un watcher (observateur) :

  • Un observateur n’observe qu’un seul type d’événement.
  • Un observateur est l’œil d’un unique effet : il intercepte tous les événements se produisant.
  • Lorsque le observateur voit une réponse du bon type il peut appeler une fonction pour savoir s’il est nécessaire de déclencher l’effet ou le déclencher sans test préalable.

Enfin, il faut réaliser une dernière classe pour synchroniser tous les événements, effets et leurs observateurs respectifs, déclencher les callbacks nécessaires, etc…

Grâce à ce système, il est possible de déclencher un événement, qui à son tour déclenchera d’autres événements, qui… et ainsi de suite…

Exemple

On implémente un système de points dans un jeu pong (l’exemple ne présente aucune utilité pratique, c’est pour expliquer le fonctionnement) :

  • A chaque fois que la balle sors de la table, le programme déclenche un événement de type : « BallIsOut » et contenant dans les données l’index du joueur
  • On implémente un effet UpdateScore qui appelle une fonction incrémentant le score du joueur correspondant à l’index passé en paramètre et qui déclenche ensuite un événement « ScoreUpdated »
  • On implémente un observateur pour les événements du type « BallIsOut » qu’on lie à l’effet UpdateScore.

Graphiquement :

*Le joueur 1 marque un point contre le joueur 2*
*Le programme déclenche un événement : BallIsOut {2}*

BallIsOut {2}   //Entre crochets nous avons les données contenu dans l'événement (en l’occurrence l'index du joueur 2)
        |
        |  Événement interceptée par l'observateur de l'effet UpdateScore
        |
        v
 UpdateScore(2)   ---- déclenche événement --->  ScoreUpdated {2, <nouveau-score>}

On peut ensuite imaginer répéter le processus pour obtenir par exemple :

BallIsOut {index_joueur}
        |
        |  Événement intercepté par l'observateur de l'effet UpdateScore
        |
        v
 UpdateScore(index_joueur)  ---- déclenche événement ---> ScoreUpdated {index_joueur, score_joueur}
                                                                       |
                                                                       |  Événement intercepté par l'observateur de l'effet Win
                                                                       |  L'observateur possède une condition : score_joueur > 5
                                                                       |
                                                                       v
                                                           Win(index_joueur)   -----> Win {index_joueur}

On a ici une cascade d’événement avec la mise à joueur du score, et la victoire du joueur ayant mis le point si son score est supérieur à 5.

Implémentation

Voici l’exemple d’un programme effectuant un scanf puis printf du nom de l’utilisateur.

        public static void DisplayName(string name)
        {
            Console.WriteLine("Bonjour {0}", name);
        }

        public static void AskName()
        {
            Console.WriteLine("Quel est ton nom ?");
            string name = Console.ReadLine();
            /*
             * On déclenche un événement de type NameWritten
             * contenant le nom de l'utilisateur
             */
            EventManager.RaiseEvent("NameWritten", name);
        }

        static void Main(string[] args)
        {
            /*
             * On ajoute un watcher qui observera les événements du type
             * start et qui entraînera un effet qui appellera la procédure AskName
             */
            EventManager.AddWatcher("start", Effect.Create(AskName));

            /*
             * Idem mais avec les événements du type NameWritten et la
             * procédure DisplayName
             */
            EventManager.AddWatcher("NameWritten", Effect.Create(DisplayName));

            //On déclenche un événement de type start sans aucune donnée
            EventManager.RaiseEvent("start");

            Console.Read();
        }

Conclusion

Une application réelle de ce système comprend des centaines d’effets possédant plusieurs observateurs, ce qui correspond en réalité à la propagation d’un événement au sein d’un graphe d’événements. J’ai choisi d’utiliser des chaînes de caractères transitant entre les événements pour modéliser les relations entre les nœuds.

Cela rend le système un poil plus lent pour les puristes (à cause de l’utilisation d’un dico et d’un style de programmation par endroits réflexif) mais possède l’avantage de le rendre plus simplement compatible avec un système de script, qui permet alors de multiples possibilités.

Je publierai les sources et toute une doc quand j’aurais trouvé le temps de me créer un repo, que tout sera joliment codé.