Simulation de TRON - PC2RON


Le code source complet des programmes

telecharger le code source de pc2ron


- Le serveur

- Le client

- Les joueurs


PC2RON est une simulation graphique de TRON sous la forme d'un jeu multi-joueurs à architecture client/serveur. Le serveur est écrit en Java et le client est écrit en langage C. La communication entre le serveur et le client se fait à travers les sockets TCP/IP en utilisant des trames personnalisées comme moyen de transport. Une trame est une succession de données sous la forme d'un tableau d'octets où chaque case du tableau représente une donnée ou une partie d'une donnée. Un donnée à sa propre taille (1, 2, 3 octet(s) ... etc). Le serveur permet de synchroniser les joueurs, il reçoit un ordre d'un joueur puis calcule et envoie les nouvelles positions à tous les joueurs, il traite aussi les collisions entre les joueurs. Un joueur envoie un ordre au serveur et attend la réponse de ce dernier.

1. Description générale du problème :

On veut réaliser un jeu multi-joueurs à architecture client/serveur en utilisant des langages différents. Plusieurs problèmes se posent : Comment permettre la communication entre deux langages de nature différente qui ont une structure différente ? Comment synchroniser les joueurs durant la partie ? Comment gérer les différents états d'une partie (pause, reprise, victoire, mort, collisions … etc) ? Pour pouvoir implémenter cette architecture, il faut répondre à toutes ces questions. Pour faciliter la tâche, le problème est décomposé en des sous-problèmes et pour chaque sous-problème y a une fonction qui le résout, du coup ça permet de minimiser le code et de détecter rapidement les erreurs puisque l'unité maintenant est la fonction et pas le programme.

2. Fonctions utilisées :

Comme la plupart des fonctions du serveur et des clients sont presque quasiment les mêmes (par exemple dans le serveur si on a la fonction sendTrame alors dans les clients on aura forcément une fonction receiveTrame qui à un corps ressemblant à sa précédente), on va décrire uniquement les fonctions qui ne se répètent pas. Dans toutes les fonctions, 'trame' représente la trame utilisée, 'csock' représente le socket client, 'joueur' est le joueur concerné et enfin 'j' est le numéro du joueur.
Les fonctions utilisées sont les suivantes :

Fonction qui permet de recevoir la trame (Initiate) d'un joueur :
int receiveInitiate(unsigned char *trame, SOCKET csock);

Fonction qui permet de recevoir la trame (Connect) d'un joueur :
int receiveConnect(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet de recevoir un ordre d'un joueur :
void receiveOrder(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet d'envoyer la trame (Ack) à un joueur :
void sendAck(unsigned char *trame, SOCKET csock, int j);

Fonction qui permet d'envoyer la trame (Alternative) à un joueur :
void sendAlternative(unsigned char *trame, SOCKET csock, char *message, int j);

Fonction qui permet d'envoyer la trame (Registred) à un joueur :
void sendRegistred(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet d'envoyer la trame (User) à un joueur :
void sendUser(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet d'envoyer la trame (End) à un joueur :
void sendEnd(unsigned char *trame, SOCKET csock, int j);

Fonction qui permet d'envoyer la trame (Pause) à un joueur :
void sendPause(unsigned char *trame, SOCKET csock, char *message, int j);

Fonction qui permet d'envoyer la trame (Start) à un joueur :
void sendStart(unsigned char *trame, SOCKET csock, char *message, int j);

Fonction qui permet d'envoyer la trame (Turn) à un joueur :
void sendTrame(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet d'envoyer la trame (Win) à un joueur :
void sendWin(unsigned char *trame, SOCKET csock, Joueur *joueur, int j);

Fonction qui permet d'envoyer la trame (Death) à un joueur :
void sendDeath(unsigned char *trame, SOCKET csock, int j);

Fonction qui permet de calculer les nouvelles positions d'un joueur :
void calculatePosDir(unsigned char *trame, SOCKET csock, Joueur
*joueur, int j, char *ordre, int *compteur);


Fonction qui permet de vérifier si c'est la fin de transmission ou non :
void isFinTransmission(unsigned char *trame, SOCKET csock);

Fonction qui permet d'afficher une trame reçue :
void trameHandler(unsigned char *trame, int taille);

Méthode qui permet d'instancier un client :
public Client() throws Exception;

Méthode qui permet de calculer la position d'un joueur à partir de sa direction :
void PosHandler(byte dir, int j);

Méthode qui permet de dessiner les joueurs, la grille, les statistiques ... etc :
public void paintComponent(Graphics g);

Méthode qui permet de traiter l'appuie sur les touches du clavier :
public void keyPressed(KeyEvent e);

Méthode qui permet de fermer le socket client :
void closeAll() throws Exception;

3. Description du code :

On commence d'abord par créer le socket serveur, faire le bind et le listage puis la création d'un thread pour chaque joueur (2 en tout). Ensuite chaque thread initialise les valeurs par défaut du joueur qu'il traite c'est à dire la position du joueur au moment où on lance la partie puis il crée le socket client et attend la connexion de ce dernier. Une fois connecté, un client commence par envoyer la trame 'Initiate', le serveur répond par une trame 'Ack' si tout va bien sinon il répond par une trame 'Alternative' (si le protocole n'est pas bon par exemple), ensuite le serveur attend la réception de la trame 'Connect' du client, quand cette dernière est reçue, le serveur envoie au client la trame 'Registred' sinon il envoie la trame 'Alternative' (si la couleur RVB du joueur est incorrecte par exemple), enfin le serveur doit attendre l'arrivée de tous les joueurs, pour cela on utilise les variables de condition. Quand tous les joueurs sont arrivés, le serveur envoie les trames 'User' et 'End' pour chaque joueur puis il envoie les messages de décompte sous la forme d'une trame 'Pause' (3... 2... 1... GO!). A ce moment là on commence à calculer le temps du jeu en utilisant la fonction clock. Tant que la partie n'est pas finie, le serveur boucle toujours en envoyant les nouvelles positions et/ou directions des joueurs puis en attendant la réception d'un ordre des joueurs (trame 'Order'). Du coté client, il crée et affiche sa fenêtre de jeu puis il active la méthode d'écoute des touches du clavier KeyListener afin de pouvoir récupérer les touches saisies au clavier ce qui permet l'envoie d'un ordre au serveur. Tout comme le serveur, le client boucle toujours en attendant la réception des nouvelles positions et/ou directions du serveur puis il redessine l'interface graphique du joueur avec la méthode 'paintComponent' pour actualiser l'affichage. Les positions et les directions des joueurs sont sauvegardés dans des tableaux ou des listes afin de pouvoir redessiner les traces des joueurs d'une part et de pouvoir calculer facilement les collisions d'autre part. Pour cela, le serveur effectue une vérification des collisions avant chaque envoie d'une trame au client, cette vérification consiste simplement à vérifier si la nouvelle position d'un joueur n'existe pas déjà dans la liste, si elle existe ça veut dire qu'un joueur est déjà passé par là donc y a forcément une collision (puisque chaque joueur garde sa trace sous la forme d'un mur d'énergie), le serveur envoie alors la trame 'Death' et/ou la trame 'Win' aux joueurs concernés. Le serveur vérifie aussi qu'un joueur n'est pas en dehors de la grille, pour cela on connaît la dimension de la grille et donc on peut facilement détecter les joueurs qui sont hors-grille. Les messages envoyés par le serveur et reçues par les clients sont affichés automatiquement dans l'interface de leur joueurs (les messages de décompte, le temps écoulé, la position actuelle des joueurs, les couleurs choisies, les identifiants, les noms des joueurs, l'état des joueurs et enfin les raccourcis clavier). On a fixé le déplacement d'un joueur (l'écart entre la dernière et la nouvelle position) à '8' (valeur entière) et la durée d'un instant à : 250000 microseconde. Le choix de cette durée est fait après plusieurs tests, si vous constatez que le jeu est trop rapide et que vous n'arrivez pas à contrôler les joueurs correctement vous pouvez modifier cette valeur dans le code du serveur (fonction calculatePosDir) ce qui permet de changer la vitesse du jeu mais ça peut aussi poser des problèmes d'incohérence si la valeur choisie est trop petite ou trop grande.

4. Instructions :

- Pour guider un joueur dans la partie, utilisez les flèches du clavier Haut, Bas, Gauche et Droite. Pour abandonner le jeu, tapez A.

- Vous pouvez si vous voulez lancer le serveur et les clients dans des machines différentes, pour cela il faut remplacer l'appel htonl(INADDR_ANY) lors de la configuration du serveur par inet_addr(IP_SERVEUR) pour mettre l'adresse IP réelle du serveur puisque celle par défaut est « localhost ». Il faut aussi changer « host » dans les clients pour mettre la nouvelle IP du serveur. Il faut aussi lancer un seul client par machine puisque le script par défaut lance tous les clients à la fois, pour cela exécutez le script : ./run_client_alone

- La vitesse du jeu est contrôlée par le serveur, ce qui veut dire que les joueurs sont toujours en état de marche mais bien sûr s'ils veulent changer la direction ils n'ont qu'à envoyer un ordre au serveur.

5. Exemple d'une partie de jeu :

pc2ron