Comme j'ai la flemme de faire de l'UML ce soir, je vais juste parler de l'idée de base, qui ne sera du coup pas forcément très claire pour le moment (voire pas du tout ), surtout si vous n'êtes pas habitués à la conception de jeux vidéo. J'essaierai de me motiver à compléter ça avec l'UML et les dessins correspondants demain.
(ps : et à finir de blablater, vu que je me suis arrêté au milieu :p)
Les principaux "modules" :
- un ensemble de données (la carte avec les groupes dessus), accessibles par l'ensemble des autres modules via une méthode statique d'une classe servant à les gérer (un singleton).
- moteur de jeu : il s'assure du respect des règles du jeu (cad vérifier qu'un déplacement ou une attaque proposée par l'utilisateur est valide par ex), de calculer ce qu'il y a à calculer (déroulement des batailles par ex), de l'IA, etc.
- moteur graphique : chargé d'afficher le jeu, d'écouter les évènements clavier, souris, et de les traduire en données utilisables par le moteur de jeu :
+ ex : faire le passage de coordonnées graphiques en coordonnées sur la carte de jeu.
+ autre ex : traduire l'appui sur la touche "Echap" par un évènement "machin veut fermer le jeu".
- moteur audio : on s'en fout (presque) complètement, mais il faut prévoir qu'il puisse y en avoir un un jour.
- moteur réseau : on s'en fout complètement aussi, mais idem, il faut envisager l'inenvisageable
Communication entre les moteurs :
- Toutes les communications entre les moteurs se font à l'aide d'objets "Event" (vu qu'il est question d'évènementiel). Ils contiennent plusieurs attributs :
+ source de l'évènement (string)
+ type de l'évènement (string) : un identificateur permettant de savoir quelles seront les clés utiles dans les données ci-dessous
+ données (map<string,string>)
+ données (map<string,int>)
+ et si le besoin s'en fait sentir : données (map<string,float>)
Cette structure permet de faire passer n'importe quel type d'information d'un moteur à l'autre, et surtout, elle le fait sans préjuger du protocole utilisé, ce qui est important dans la mesure où il est appelé à grandir considérablement au cours du développement.
La communication d'Event entre deux moteurs se fait selon le pattern "Observateur" (mais pas exactement, j'en parlerai après). Je vais essayer de commencer par le début :
-A l'initialisation du programme, on fait en sorte que les moteurs soient capables de s'écouter entre eux. Par exemple :
- moteurJeu.addEcouteur(moteurGraphique);
- moteurJeu.addEcouteur(moteurAudio);
- moteurJeu.addEcouteur(moteurReseau);
- moteurGraphique.addEcouteur(moteurJeu);
- moteurGraphique.addEcouteur(moteurAudio);
- moteurGraphique.addEcouteur(moteurReseau);
- et idem pour les deux autres moteurs.
A l'issue de tout ce schmilblick, admettons qu'une bataille entre deux groupes soit calculée par le moteur de jeu. Il créera pour l'occasion un évènement disant (je traduis en Français) "il y a eu une bataille entre le groupe situé en (12,3) et celui situé en (12,4), pendant le tour de Mr. Machin. Il reste 31 soldats à l'un et 47 à l'autre", et l'enverra à qui veut bien l'entendre. Ces gens qui veulent bien l'entendre, c'est justement les trois autres moteurs, qui chacun traiteront l'évènement à leur manière, en fonction de leurs responsabilités :
- Le moteur graphique mettra à jour l'affichage des deux groupes concernés. C'est son job.
- Le moteur audio pourra jouer un son "Mange toi ça face d'huître", "Aïeuuuh !", ou je ne sais quoi pour agrémenter l'action (juste pour l'exemple, vu qu'on se moque pas mal de l'audio en fait)
- Le moteur réseau sérialisera l'évènement et l'enverra par le réseau aux autres joueurs, ce qui permettra une synchronisation des données (mais c'est juste pour l'exemple aussi, faudrait être fou pour se lancer dans du réseau )
Donc, j'ai parlé d'envoyer un évènement, mais je n'ai pas dit comment. Une façon simple pourrait être d'utiliser deux fonctions communes aux moteurs :
- envoyerEvenement(Event) : appelle la fonction onEvent(Event) de tous les écouteurs de ce moteur
- onEvent(Event) : traite l'évènement passé en paramètre. Cette méthode est appelée "à distance" par les autres moteurs lorsqu'ils veulent transmettre des informations.
On a donc à faire à du pattern Observateur... Mais en fait, c'est mal !
En effet, si on fait ça, tout se passera dans le même thread. C'est pas un problème en soi, mais nous, on aura des traitements lourds (l'IA). Donc quand l'IA travaillera, l'interface freezera lamentablement. On a connu mieux... Il faut donc que la communication entre les moteurs soit asynchrone, ou dit autrement, que chacun ne travaille que dans des threads qui lui sont propres. Là aussi, ça serait bien plus clair en dessin, mais la flemmeuuuuh :'(
Je vais quand même essayer d'expliquer vite fait les étapes :
- les moteurs sont dotés, en plus du reste, d'une file d'objets "Event" (je ne parle pas de file comme on a vu en système, mais d'objet de la STL hein). C'est elle qui permet de rendre les communications asynchrones.
- la méthode envoyerEvenement(Event) n'appelle plus directement les fonctions onEvent(Event) des écouteurs, mais à la place une fonction enfilerEvent(Event). Cela aura pour effet de mettre l'évènement dans la file des moteurs cibles, et c'est tout ! Fin du travail pour le thread expéditeur du message, qui peut directement passer à autre chose => pas de blocage.
- les threads des moteurs destinataires sont alors réveillés, puisqu'un Event est arrivé dans la file. Ils l'en sortent donc, et le traitent (sans emmerder les autres donc).
Résolution des petits problèmes liés au multithreading
En fait, il n'y en a pas tant que ça, vu que les données des différents moteurs sont bien séparées entre elles, et qu'au sein des moteurs, il n'y a pas 36000 threads qui se les disputent. Par contre :
- multithreading oblige, l'affichage du jeu peut se faire en même temps que des modifications des données (la position des groupes par exemple). Si rien n'est fait, on pourra potentiellement se retrouver avec le même groupe présent dans deux cases différentes (c'est juste un exemple, je suis sûr qu'il y aurait plein d'autres bugs marrants ). Il faut donc que les données utilisées par le moteur graphique soient figées le temps de l'affichage des animations. Pour cela, on ne peut pas utiliser de verrou : cela rendrait les threads totalement inutiles. Il faut donc pouvoir copier "en dur" les données du "plateau de jeu" (ce qui reste quand même très léger, vu qu'il n'est pas nécessaire de mettre en dur dans les objets "Groupe" les images associées). Cela peut se faire via le singleton gérant les données (une méthode getCopie() par ex...).
- Plusieurs threads peuvent en même temps tenter d'ajouter un évènement à la file d'un même moteur. Que se passe t-il dans ce cas ? Bonne question ! Je ne sais pas si cette opération est thread-safe, mais si elle ne l'est pas, il faudra verrouiller tout ça.
(ps : et à finir de blablater, vu que je me suis arrêté au milieu :p)
Les principaux "modules" :
- un ensemble de données (la carte avec les groupes dessus), accessibles par l'ensemble des autres modules via une méthode statique d'une classe servant à les gérer (un singleton).
- moteur de jeu : il s'assure du respect des règles du jeu (cad vérifier qu'un déplacement ou une attaque proposée par l'utilisateur est valide par ex), de calculer ce qu'il y a à calculer (déroulement des batailles par ex), de l'IA, etc.
- moteur graphique : chargé d'afficher le jeu, d'écouter les évènements clavier, souris, et de les traduire en données utilisables par le moteur de jeu :
+ ex : faire le passage de coordonnées graphiques en coordonnées sur la carte de jeu.
+ autre ex : traduire l'appui sur la touche "Echap" par un évènement "machin veut fermer le jeu".
- moteur audio : on s'en fout (presque) complètement, mais il faut prévoir qu'il puisse y en avoir un un jour.
- moteur réseau : on s'en fout complètement aussi, mais idem, il faut envisager l'inenvisageable
Communication entre les moteurs :
- Toutes les communications entre les moteurs se font à l'aide d'objets "Event" (vu qu'il est question d'évènementiel). Ils contiennent plusieurs attributs :
+ source de l'évènement (string)
+ type de l'évènement (string) : un identificateur permettant de savoir quelles seront les clés utiles dans les données ci-dessous
+ données (map<string,string>)
+ données (map<string,int>)
+ et si le besoin s'en fait sentir : données (map<string,float>)
Cette structure permet de faire passer n'importe quel type d'information d'un moteur à l'autre, et surtout, elle le fait sans préjuger du protocole utilisé, ce qui est important dans la mesure où il est appelé à grandir considérablement au cours du développement.
La communication d'Event entre deux moteurs se fait selon le pattern "Observateur" (mais pas exactement, j'en parlerai après). Je vais essayer de commencer par le début :
-A l'initialisation du programme, on fait en sorte que les moteurs soient capables de s'écouter entre eux. Par exemple :
- moteurJeu.addEcouteur(moteurGraphique);
- moteurJeu.addEcouteur(moteurAudio);
- moteurJeu.addEcouteur(moteurReseau);
- moteurGraphique.addEcouteur(moteurJeu);
- moteurGraphique.addEcouteur(moteurAudio);
- moteurGraphique.addEcouteur(moteurReseau);
- et idem pour les deux autres moteurs.
A l'issue de tout ce schmilblick, admettons qu'une bataille entre deux groupes soit calculée par le moteur de jeu. Il créera pour l'occasion un évènement disant (je traduis en Français) "il y a eu une bataille entre le groupe situé en (12,3) et celui situé en (12,4), pendant le tour de Mr. Machin. Il reste 31 soldats à l'un et 47 à l'autre", et l'enverra à qui veut bien l'entendre. Ces gens qui veulent bien l'entendre, c'est justement les trois autres moteurs, qui chacun traiteront l'évènement à leur manière, en fonction de leurs responsabilités :
- Le moteur graphique mettra à jour l'affichage des deux groupes concernés. C'est son job.
- Le moteur audio pourra jouer un son "Mange toi ça face d'huître", "Aïeuuuh !", ou je ne sais quoi pour agrémenter l'action (juste pour l'exemple, vu qu'on se moque pas mal de l'audio en fait)
- Le moteur réseau sérialisera l'évènement et l'enverra par le réseau aux autres joueurs, ce qui permettra une synchronisation des données (mais c'est juste pour l'exemple aussi, faudrait être fou pour se lancer dans du réseau )
Donc, j'ai parlé d'envoyer un évènement, mais je n'ai pas dit comment. Une façon simple pourrait être d'utiliser deux fonctions communes aux moteurs :
- envoyerEvenement(Event) : appelle la fonction onEvent(Event) de tous les écouteurs de ce moteur
- onEvent(Event) : traite l'évènement passé en paramètre. Cette méthode est appelée "à distance" par les autres moteurs lorsqu'ils veulent transmettre des informations.
On a donc à faire à du pattern Observateur... Mais en fait, c'est mal !
En effet, si on fait ça, tout se passera dans le même thread. C'est pas un problème en soi, mais nous, on aura des traitements lourds (l'IA). Donc quand l'IA travaillera, l'interface freezera lamentablement. On a connu mieux... Il faut donc que la communication entre les moteurs soit asynchrone, ou dit autrement, que chacun ne travaille que dans des threads qui lui sont propres. Là aussi, ça serait bien plus clair en dessin, mais la flemmeuuuuh :'(
Je vais quand même essayer d'expliquer vite fait les étapes :
- les moteurs sont dotés, en plus du reste, d'une file d'objets "Event" (je ne parle pas de file comme on a vu en système, mais d'objet de la STL hein). C'est elle qui permet de rendre les communications asynchrones.
- la méthode envoyerEvenement(Event) n'appelle plus directement les fonctions onEvent(Event) des écouteurs, mais à la place une fonction enfilerEvent(Event). Cela aura pour effet de mettre l'évènement dans la file des moteurs cibles, et c'est tout ! Fin du travail pour le thread expéditeur du message, qui peut directement passer à autre chose => pas de blocage.
- les threads des moteurs destinataires sont alors réveillés, puisqu'un Event est arrivé dans la file. Ils l'en sortent donc, et le traitent (sans emmerder les autres donc).
Résolution des petits problèmes liés au multithreading
En fait, il n'y en a pas tant que ça, vu que les données des différents moteurs sont bien séparées entre elles, et qu'au sein des moteurs, il n'y a pas 36000 threads qui se les disputent. Par contre :
- multithreading oblige, l'affichage du jeu peut se faire en même temps que des modifications des données (la position des groupes par exemple). Si rien n'est fait, on pourra potentiellement se retrouver avec le même groupe présent dans deux cases différentes (c'est juste un exemple, je suis sûr qu'il y aurait plein d'autres bugs marrants ). Il faut donc que les données utilisées par le moteur graphique soient figées le temps de l'affichage des animations. Pour cela, on ne peut pas utiliser de verrou : cela rendrait les threads totalement inutiles. Il faut donc pouvoir copier "en dur" les données du "plateau de jeu" (ce qui reste quand même très léger, vu qu'il n'est pas nécessaire de mettre en dur dans les objets "Groupe" les images associées). Cela peut se faire via le singleton gérant les données (une méthode getCopie() par ex...).
- Plusieurs threads peuvent en même temps tenter d'ajouter un évènement à la file d'un même moteur. Que se passe t-il dans ce cas ? Bonne question ! Je ne sais pas si cette opération est thread-safe, mais si elle ne l'est pas, il faudra verrouiller tout ça.
Dernière édition par Cédric le Mar 27 Jan - 23:13, édité 1 fois