Tutorial : 39Dll pour des jeux multijoueurs
Ce tutoriel est destiné à permettre à tous de créer des jeux multijoueurs facilement grâce à la dll 39Dll. Notre objectif dans ce tuto sera simple nous allons créer un "jeu" utilisant une relation serveur/clients.
J'ai décidé de m'écarter du Pong habituel pour la base des jeux en réseau pour une raison simple : il ne permet pas l'apprentissage des méthodes de la gestion en masse de clients dont le nombre peut varier à tout instant. Nous ferons donc quelque chose de simple : grâce aux sprites gracieusement fournis par GM, nous ferons 2 programmes : - le serveur : il affichera la liste des joueurs accompagnés de leurs ids et de leurs coordonnées x et y - le client (que l'on pourra démarrer autant de fois que l'on veut) : qui affichera le personnage sous la forme d'un petit fantôme rouge (sprites de pacman xD ) et les autres joueurs sous la forme de petits fantômes verts !
Bien, commençons ...
I. Établir la connexion Nous allons commencer avec le serveur, prenez un projet vierge accompagné des tous les scripts de la Dll pour la version 6.1 de GM, ou avec le GEX 39Dll pour GM 7.0 (disponible sur le site du CBNA il me semble). J'ai fait ce tuto avec la version 6.1 j'espère donc que dans le GEX les noms de fonctions sont les mêmes. Donc, créez un objet appelé "controlleur" (ou tout ce que vous voulez), ce sera le seul objet de ce programme. Dans l'évènement "create" de cet objet mettez la pièce de code suivante :
Citation:
cette fonction sert à initialiser la DLL, elle prend 3 arguments : le premier est l'emplacement de la DLL dans l'ordinateur, mettez 0 si le fichier se nomme "39dll.dll" et qui est dans le répertoire du jeu. Le deuxième argument vous demande si oui ou non (true ou false) vous voulez charger les fonction d'envoi de données via internet (nous on en a besoin) et le troisième si vous voulez charger les fonction de cryptage et autres, inutile pour nous. Toujours dans l'event "create", maintenant que la dll est chargée, nous allons écouter un port. Je m'explique : l'ordinateur possède plusieurs entrée/sorties virtuelles le reliant à internet, elle sont numérotée de 1 à ... plus de 50.000 je pense, et son appelées les ports. (par exemple le port n°80 est réservé à la connexion vers les sites web, via firefox, internet explorer, etc ...) donc choisissons un port par exemple le n°12345, c'est via ce port que nos 2 programmes vont "discuter". donc écoutons grâce à ce code :
Citation: global.socket=tcplisten(12345,10,1);
if(!global.socket) { show_message("Impossible d'ouvrir le port n°12345"); game_end(); } |
ici la fonction utilisée est tcplisten, elle comporte elle aussi 3 arguments : le n° de port à ouvrir, la longueur de la "fille d'attente" (c'est le nombre maximal de programmes pouvant être en attente, ça n'a rien à voir avec le nombre de personnes pouvant jouer au jeu, comme il est souvent dit, je l'ai testé) et le troisième argument est si la fonction doit être bloquante (0) ou non-bloquante (1). (Si la fonction est bloquante, elle empêchera la suite du code de s'exécuter jusqu'à ce qu'elle reçoive une connexion, en fait, cet argument affectera la fonction tcpaccept que l'on verra plus tard). Cette fonction renvoie l'id du socket ainsi créé, ou un code d'erreur négatif, d'où la vérification faisant quitter le programme en cas d'erreur /! attention : le port que vous utilisez pour écouter doit être laissé libre par votre pare-feu ! Maintenant, créons la liste des joueurs, dans laquelle le serveur enregistrera les IDs des joueurs. Nous pourrions utiliser un tableau (joueur[1], joueur[2], etc ...) mais il y a un inconvénient majeur, si des joueurs quittent en pleine partie, les emplacements de données utilisés pour eux ne serons pas vidés, et le serveur va accumuler les zones de mémoire utilisées pour rien, ce qui n'est pas très bon, et qui en plus rend le traitement des données infernal ... Je me suis donc penché sur l'utilisation des Listes (ds_list_...) ce sont des tableaux à une dimension qui, lorsqu'on supprime une donnée se réorganisent (si je supprime l'entrée n°2, la 3 devient 2, la 4 devient 3, etc ...), tout bénef pour nous. (je vous invite donc à lire la partie "listes" de l'aide de GM) Créons donc la liste et initialisons une variable qui nous servira plus tard :
Citation: global.listsockets = ds_list_create();
nbjoueurs = 0; |
Voilà, normalement votre code create est donc ceci :
Citation: dllinit(0,true,false); global.socket=tcplisten(12345,10,1); if(!global.socket) { show_message("Impossible d'ouvrir le port n°12345"); game_end(); } global.listsockets = ds_list_create(); nbjoueurs = 0;
|
maintenant, passons à l'event "steps" : nous allons utiliser une nouvelle fonction : tcpaccept. Cette fonction renvoie l'ID du programme nouvellement accepté, ou bien 0 ou un code d'erreur négatif. donc appelons cette fonction et si le code est positif, réons une nouvelle entrée dans la liste de sockets : socketjoueur = tcpaccept(global.socket,1) // arguments : socket du port à utiliser, bloquant/non-bloquant.
Citation: if(socketjoueur>0)
{ ds_list_add(global.listsockets,socketjoueur); //ajoutons ce socket à la liste de sockets. } |
maintenant, je vais vous faire mettre un code sans l'expliquer, (je sais ce n'est pas bien) je ne l'expliquerais que lors de la partie II : les messages. Ce code sert à supprime automatiquement de la liste un joueur déconnecté.
Citation: nbjoueurs = ds_list_size(global.listsockets); for(i=0;i<=nbjoueurs-1;i+=1) { while(1) { size = receivemessage(ds_list_find_value(global.listsockets,i)); if(size < 0) break; if(size == 0) { ds_list_delete(global.listsockets,i); break; } } } |
voila maintenant votre code "steps" :
Citation: socketjoueur = tcpaccept(global.socket,1) // arguments : socket du port à utiliser, bloquant/non-bloquant. if(socketjoueur>0) { ds_list_add(global.listsockets,socketjoueur); //ajoutons ce socket à la liste de sockets. }
// code pour supprimer les joueurs déconnectés nbjoueurs = ds_list_size(global.listsockets); for(i=0;i<=nbjoueurs-1;i+=1) { while(1) { size = receivemessage(ds_list_find_value(global.listsockets,i)); if(size < 0) break; if(size == 0) { ds_list_delete(global.listsockets,i); break; } } } |
puis dans l'event "game end" mettez le code
Citation:
pour libérer la mémoire de la dll.
c'est tout pour l'instant pour le serveur.
nous allons passer aux fonctions de connexion du client.
ouvrez un nouveau projet avec les scripts/le GEX de la 39DLL, et créez-y 2 rooms ("waiting" et "game") et 1 objet ("control") persistant et placé dans la 1ere room.
dans le code "create" :
Citation: global.server=tcpconnect(127.0.0.1,12345,1); if(global.server<=0) { show_message("Impossible de se connecter à 127.0.0.1:12345"); game_end(); } room_goto_next(); |
la fonction est tcpconnect avec 3 arguments : l'ip du serveur, le port à utiliser, bloquant/non-bloquant.
si c'est effectif, on passe dans la room suivante, ou il y aura le jeu. (que l'on fera dans la partie n° 2)
l'objet control doit être persistant à cause du code placé dans l'event "game end" :
Citation:
voilà c'est tout pour la partie connexion, vous pouvez tester en démarrant le serveur puis 2 ou 3 fois le client, pour bien voir, mettez les rooms de tous les jeux taille 300x300, et ajoutez ce code dans le "draw event" du serveur :
Citation: draw_text(10,10,"id"); draw_line(42,0,42,300);
draw_line(0,30,300,30);
for(k=0;k<=nbjoueurs-1;k+=1) { draw_text(10,20*(k+2),ds_list_find_value(global.listsocks,k)); }
|
c'est une boucle ui affiche la liste des ids de joueurs connectés.
II. Les messages et les buffers
Voilà maintenant la 2e partie, où nous allons traiter du plus important : l'envoi de messages. il se fait en 4 étapes : niveau expéditeur : écriture sur le buffer => envoi du contenu du buffer niveau destinataire : réception du message dans le buffer => traitement du contenu. Le buffer est une sorte de variable assez spéciale : elle n'a pas de type vraiment définit et peut contenir plusieurs types de valeurs à la fois. Par exemple, si chaque 0 est un octet : voilà mon buffer : 0000000000
Il est décomposé ainsi : 00(nombre) - 00000(caractères) - 000(nombre) mais peut l'être différemment, ainsi, le destinataire doit savoir de quoi est composé le buffer ! Je vais donc vous faire la liste des types de contenus possible ainsi que les fonctions servant à les lire et les écrire.
- les bytes (octets en français) = un nombre entier compris entre 0 et 255 (fonctions : writebyte(valeur) et reabyte() ) taille : 1 octet
- les shorts (courts en français) = un nombre entier compris entre -32.768 et 32.767 (fonctions : writeshort(valeur) et readshort() ) taille : 2 octets
- les unsigned shorts (courts non signés) = un nombre entier compris entre 0 et 65.536 (fonctions : writeushort(valeur) et readushort() ) taille : 2 octets
- les int (entiers) = un nombre entier compris entre -2.147.483.648 et 2.147.483.647 (fonctions : writeint(valeur) et readint(valeur) ) taille : 4 octets
- les unsigned int (entiers on signés) = un nombre entier compris entre 0 and 4.294.967.296 (fonctions : writeuint(valeur) et readuint() ) taille : 4 octets
- les float (flottants) = un nombre décimal (à virgule) entrant dans 4 octets (plus il y a de chiffres après la virgule, plus le nombre sera petit, évidemment) (fonctions : writefloat(valeur) et readfloat() )
- les doubles = un nombre décimal de 8 octets (donc à éviter le plus souvent possible, correspond au maximum supporté par GM) (fonctions : writedouble(valeur) et readdouble() )
- les chars = chaine de caractères (1 octet = 1 caractère) (pour la fonction de lecture il faut préciser le nombre d'octets à lire en argument) (fonctions : writechars(valeur) et readchars(longueur) )
- les strings = chaine de caractères terminée par le caractère NULL (la fonction de lecture s'y arrète, il n'est plus nécéssaire de préciser le nombre de caractères à lire) (fonctions : writestring(valeur) et readstring() )
Comme vous le voyez, il y a le choix. il faut donc bien choisir les types de données à choisir : pour les coordonnées d'un objet dans une room, un short suffit, alors que les doubles ne devraient être utilisés que pour le transfert de données scientifiques de haute précision ... Maintenant, voilà un code d'écriture basique :
Citation: clearbuffer(); // on vide le buffer writeshort(x); writeshort(y); // on écrit les coordonnées sendmessage(socketid); // on envoie |
le code de base de réception est plus complexe :
Citation: while(1) // boucle infinie { size = receivemessage(socketid); // retourne le nombre de bytes reçus ou un code d'erreur if(size<0){break;} // pas de message reçu, on quitte la boucle if(size==0) // le joueur s'est déconnecté { //actions si le joueur est déconnecté (on supprime/sauvegarde ses infos) exit; }
// actions de traitement
} |
normalement, il doit vous rappeler quelque chose : le code utillisé dans la partie I pour savoir si le joueur était déconnecté !
Maintenant, tout s'éclaircit non ?
Retournons à notre jeu : prenons le client, créons un objet joueur (avec le sprite de monstre rouge pacman ^^) que nous mettons dans la room "game", la deuxième, faisons un code rapide de déplacement :
Citation: //keyboard up if(y>0) { y-=4; } //keyboard down if(y<268) // rappelez vous ! les rooms font 300x300px et le sprite 32x32px { y+=4; } //keyboard left ifx>0) { x-=4; } //keyboard right if(x<268) { x+=4; } |
Voilà et maintenant, il a falloir qu'à chaque step, l'objet envoi sa position au serveur : on va faire un code tout simple en se rappelant ce que j'ai dit plus haut :
Citation: clearbuffer(); // on vide le buffer writeshort(x); writeshort(y); // on écrit les coordonnées sendmessage(global.server); // on envoie avec le socket du serveur |
voilà, c'est tout pour l'instant pour le client
Revenons au serveur, il va falloir transformer le code de réception précédent (je vous le rappelle) :
Citation: nbjoueurs = ds_list_size(global.listsockets); for(i=0;i<=nbjoueurs-1;i+=1) { while(1) { size = receivemessage(ds_list_find_value(global.listsockets,i)); if(size < 0) break; if(size == 0) { ds_list_delete(global.listsockets,i); exit; } } } |
je pense que vous le comprenez maintenant : la boucle for permet de traiter tous les joueurs dont on reçoit l'un après l'autre les messages.
Mais avant, il nous faut créer 2 autre listes, pour stoker les coordonnées x et y de chaque joueur, donc dans l'event create on doit avoir à la fin :
Citation: global.listsockets = ds_list_create(); global.listx = ds_list_create(); global.listy = ds_list_create(); nbjoueurs = 0;
|
et on revoit aussi le code de steps pour accepter les nouveaux joueurs :
Citation: socketjoueur = tcpaccept(global.socket,1) if(socketjoueur>0) { ds_list_add(global.listsockets,socketjoueur); ds_list_add(global.listx,0); ds_list_add(global.listy,0); } |
Voilà. maintenant, on peut traiter les messages reçus pour mettre à jour les données du serveur.
Citation: nbjoueurs = ds_list_size(global.listsockets); for(i=0;i<=nbjoueurs-1;i+=1) { while(1) { size = receivemessage(ds_list_find_value(global.listsockets,i)); if(size < 0) break; if(size == 0) { ds_list_delete(global.listsockets,i); exit; } // traitement des données ds_list_replace(global.listx,i,readshort()); ds_list_replace(global.listy,i,readshort()); } } |
Et voilà ! Si on met à jour le code d'affichage du serveur :
Citation: draw_text(10,10,"id"); draw_line(42,0,42,300); draw_text(45,10,"x"); draw_line(77,0,77,300); draw_text(80,10,"y"); draw_line(112,0,112,300);
draw_line(0,30,300,30)
for(k=0;k<=nbjoueurs-1;k+=1) { draw_text(10,20*(k+2),ds_list_find_value(global.listsocks,k)); draw_text(45,20*(k+2),ds_list_find_value(global.listx,k)); draw_text(80,20*(k+2),ds_list_find_value(global.listy,k)); draw_line(0,20*(k+3)-3,300,20*(k+3)-3) } |
On voit s'afficher les coordonnées x et y de chaque joueurs.
Il n'y a plus qu'à ajouter 2 codes similaires dans l'autre sens (que je n'expliquerais que peu, car c'est globalement la même chose) grace auxquels le serveur envoie à chaque jouer, les coordonnées des autres joueurs, que ce dernier puisse les afficher (à l'aide du sprite pacman de monstre vert).
Code steps du serveur à mettre dans la boucle for du serveur, après la boucle while(1) :
Citation: clearbuffer(); // on vide le buffer writeshort(nbjoueurs-1); // on inscrit le nombre de joueurs à traiter (-1 car il ne se traite pas lui même) for(j=0;j<=nbjoueurs-1;j+=1) // une boucle { if(i!=j) // si le joueur dont on a les données n'est pas le joueur que l'on traite (on ne va pas lui envoyer ses propre coordonnées) { writeshort(ds_list_find_value(global.listx,j)); writeshort(ds_list_find_value(global.listy,j)); /* On écrit les données les unes après les autre ainsi : nbjoueurs | joueur1.x | joueur1.y | joueur 2.x | joueur 2.y | etc ... */ } } sendmessage(ds_list_find_value(global.listsocks,i)); // on envoie le message |
Puis un code de reception et de traitement dans la client dans steps à placer à la suite :
Citation: while(1) { size=receivemessage(global.server); // on reçoit if (size<0) break; if (size==0) // si la connection est perdue { show_message("Déconnecté du serveur."); game_end(); exit; } monsternb=readshort(); // on récupère le nombre de monstres for (i=0;i<=monsternb-1;i+=1) { monsterlist_x=readshort(); // on enregistre leurs coordonnées dans des tableaux monsterlist_y[i]=readshort(); } } |
et un code d'affichage à mettre dans le draw du monstre :
Citation: for(i=0;i<=monsternb-1;i+=1) { draw_sprite(sprite_autre_joueur0,monsterlist_x[i],monsterlist_y[i]); } draw_sprite(sprite_joueur,0,x,y); // on affiche son propre sprite |
Voilà pourquoi j'initialisait monsternb à 0 car comme 0>0-1, la boucle ne se démarre pas si le joueur est seul.
Et voilà, c'est fini grace à celà vous avez normalement compris les bases, en troisième partie je ferais une petite explication sur les possibilités de codage et d'utilisation des fichiers fournies pas la dll.
ATTENTION : pour utiliser les fonctions suivantes, il vous faudra activer la deuxième partie de la dll, utiliser la fonction dllinit(0,true,true); et non dllinit(0,true,false);
III. Cryptage
alors là ça va être très rapide, il y a deux fonctions à connaitre :
bufferencrypt(password) : code le contenu du buffer avec le mot de passe choisi (à utiliser juste avant l'envoi)
bufferdecrypt(password) : décode le contenu du buffer avec le mot de passe choisi (à utiliser juste après la réception)
Et sinon, autre chose utile : les fonctions de hashage mais avant tout qu'est-ce que je hashage ? c'est un groupe d'algorithmes qui transforment une chaine de caractères en un nombre ou une autre chaine de manière définitive. on ne peut pas revenir en arrière. Mais alors, à quoi cela peut-il bien servir ? Et bien, à rendre le transfert de données encore plus sécurisé, par exemple pour le transfert du mot de passe de connexion : le serveur le connait que le hash du mot de passe, (comme ça en cas de piratage, il ne peut pas le fournir), le client pour se connecter, hashe son mot de passe et l'envoie au serveur qui le compare à ce qu'il connait. Car chaque chaine possède un hash unique et différent des autres. Enfin, si vous voulez plus de précisions allez sur wikipédia. Ici l'algorithme utilisé est MD5, un des plus sûrs existants. Il y a deux fonctions :
md5string(chaine) = retourne le hash md5 de la chaine envoyée
md5buffer() = retourne le hash md5 du contenu du buffer [i]
(dans ce tuto j'ai utilisé le mot "hash" et le verbe "hasher" sans être sûr de l'orthographe ni même s'ils existaient, veuillez m'en excuser)
IV. Les fichiers
La 39 dll permet également l'utilisation des fichiers grace à quelques fonctions de base :
fileopen(file,mode) : 1er argument : fichier à ouvrir, 2e argument : mode (0=lecture, 1=écriture, 2=lecture et écriture), la fonction retourne l'id du fichier ouvert. fileclose(fileid) : ferme le fichier d'id fileid. filewrite(fileid) : écrit le contenu du buffer dans le fichier fileid. fileread(fileid) : copie le contenu du fichier dans le buffer filepos(fileid) : retournela position du curseur dans le fichier filesetpos(fileid,pos) : régle la position du curseur dans le fichier (0 = 1er caractère, 1= 2e caractère, etc ...) filesize(fileid) : retourne la taille du fichier en octets.
V. Les buffers multiples
39Dll permet également de travailler avec plusieurs buffers, et-ce de manière simple : createbuffer() = crée un buffer et renvoie son id freebuffer(id) = supprime le buffer choisi bufferexists(id) = retourne true si le buffer existe, false sinon.
Et, pour toutes les fonctions utilisant un buffer, ajoutez un argument contenant l'id du buffer à utiliser exemple :
writeshort(55) devient writeshort(55,bufferid) readshort() devient readshort(bufferid) le buffer 0 est le buffer utilisé par défaut, il ne peut pas être supprimé avec freebuffer(0); VI. autres fonctions utiles netconnected() = retourne true si l'ordinateur est connecté à internet lastinIP() = retourne l'ip du dernier message reçu lastinPort() = idem avec le dernier port closesocket(id) = supprime le socket choisi
Tutorial de Levans.
|