Focus TourDeJeu

par Le Caphar Dveloppeur du jeu Tratognse

[Ceci est la version complte imprimable]

Mtaphore botanique
Tout programmeur en herbe, alors qu'il plante les petites graines de ce qui deviendra un grand succs du jeu en ligne, conoit un jour des arborescences interminables et des aventures merveilleuses, aux graphismes tapageurs servis par une interface d'une intuitivit ingalable. Et lorsqu'il lance ses premiers essais sur un modeste hbergeur gratuit, bahi par la splendeur nue de son premier echo "Bonjour."; en Times noir sur fond blanc, il se rend compte - mais vaguement, sans trop vouloir y penser - que c'est assez lent quand mme. Alors il fait pousser ses fonctions, arrose sa base de donnes, et quand il a obtenu une vritable flore, il invite quelques braves passants venir cueillir quelques fruits. Oui mais voil... les fruits bourrs d'OGM mesurent trois mtres et psent six tonnes. Et les quelques badauds qui s'aventurent dans le jardin ressortent bredouilles et passablement nervs.

Cette petite mtaphore botanique, pas trs rigoureuse, nous a tout de mme conduit au coeur du sujet : un jeu en ligne trop lourd, si beau soit-il, si fascinante soit son intrigue, fera fuir les visiteurs, qui n'ont pas que a faire d'attendre le chargement de 1Mo pour dplacer leur monstre d'une case. Avec un peu de chance, vous arriverez capturer quelques joueurs. Soit, mais les ennuis ne font que commencer. Untel va vous dire que les pages ne se chargent pas assez vite, donc il clique plusieurs fois, rechargeant systmatiquement le dbut de la page d'action, ce qui finit - selon l'ordre dans lequel vous avez plac les vnements - par lui faire perdre tous ses points ou au contraire effectuer des tas d'actions gratuitement.

Un jour, vous runissez enfin les fonds pour que votre jeu dispose d'un serveur. Rapide, fiable, pas cher ; c'est le rve. Oui mais voil : au bout d'un mois, le commercial vous appelle et vous signale que vous "abusez du trafic illimit" (rigolez pas, c'est ainsi que va le commerce) qui vous est octroy, et que pour grer vos 40 GO de donnes changes par jour vous devrez hypothquer la maison le mois suivant.

"Fichtre", se dit-on dans ces cas-l. "Je n'tais pas fait pour l'herboristerie virtuelle."

Et l je vous dis : "Quid de ces rcriminations ? Mettez-vous au travail ! Un jeu c'est RIEN." Ou presque. En termes de donnes, la plupart des jeux online pourraient tenir sur une disquette, et les calculs qu'ils effectuent pourraient tre faits par un enfant de CE2 (on va dire une classe de CE2 pour les plus complexes). Comment alors parviennent-ils mettre genoux un serveur ? Parce qu'ils sont trop expansifs. Ce que je veux inculquer aux programmeurs dbutants, c'est ALLEGER leur jeu, par diverses petites ruses que j'ai apprises au fil des nuits sans sommeil, l'oeil morne riv sur d'impntrables lignes de code. Et si je pouvais donner quelques trucs aux vieux routards qui tranent, je n'en serais que plus fier.

Compactez vos donnes
Le premier rflexe que devrait avoir un concepteur de jeu, c'est de tester son jeu sur un modem 14K. Encore que ce soit difficile trouver de nos jours, l'exprience est enrichissante. Elle permet de mesurer la quantit de donnes qui sont envoyes au joueur par le jeu.

Il faut ds prsent noncer une rgle ESSENTIELLE dans la conception d'un jeu : estimez toujours la quantit de donnes affiches en fonction du temps moyen que devra passer le joueur sur la page de jeu. S'il doit s'arrter pour rflchir deux minutes entre chaque action le temps que le jeu se charge, le niveau de rflexion stratgique du jeu a intrt tre lev. Un jeu de rflexion peut impliquer des lments graphiques plus complexes qu'un jeu d'action. Et l'efficacit du jeu impose des compromis ou des ruses de sioux pour s'accomoder de graphismes complexes.

Oubliez votre fond d'cran en 1280x1024
C'est dur vivre, mais votre fond d'cran reprsentant un dragon rouge terrassant un quelconque Saint Michel va devoir passer la poubelle. Certes il ne pse que 130ko en JPEG. Mais quand 1000 joueurs viennent jouer tous les jours, ils tlchargent grosso modo 130 mgas de ce fond d'cran par jour (en comptant le cache du navigateur). Soit 4GO par mois, plus que le plafond de 3GO qu'autorisent certains hbergeurs. Les solutions pour vivre sans image de fond gante sont multiples. La premire, c'est de dcomposer l'image, par exemple entre arrire-plan et avant-plan. Si le fond de l'image est un dgrad rouge/vert du plus bel effet, il sera sans doute intressant de compresser ce fond indpendamment en JPEG et de surimposer le dragon en GIF transparent. De toute manire, un fond d'cran n'a pas besoin de sauter aux yeux puisqu'il est destin dcorer le fond de l'cran.

La variante, c'est la texture "marbre" qui pse 30ko. Certes pour avoir un motif pas trop rptitif, l'image doit tre grande, mais il est impensable que ce fond ne se rduise pas quelques ko (en GIF 12 couleurs ou en JPEG un peu compress). C'est une question de compromis : il faut accepter une petite perte de couleur dans le fond d'cran (qui est d'ailleurs souvent cach par les boutons, cadres et autres textes) quand elle permet de diviser la taille par 6. Les joueurs vous en seront ternellement reconnaissants. Il y a plein de logiciels et d'utilitaires qui permettent de grer la compression l'octet prs (Photoshop, Paint Shop Pro, JPEG Optimizer...), ne vous en privez pas.

D'une manire gnrale, dcomposer une image en petits lments permet d'conomiser des octets parce que la compression fonctionne trs bien sur des lments simples. Il suffit de les assembler ensuite dans les pages HTML constituant le jeu. Le HTML permet de surimposer le fond d'cran du BODY, puis le fond dtour (en GIF transparent) d'un TABLE, par exemple, et enfin une IMG ventuellement transparente, elle aussi. Et encore je ne parle pas de LAYERS qui permettent d'empiler les couches d'images. De quoi dcomposer en petits morceaux la plupart des lments visuels de votre jeu. Mais l'essentiel, je vous le rappelle, c'est d'tre modestes : si le logo 3D que vous avez fait ne se compresse pas en dessous de 20ko, JETEZ-LE ! Ne surchargez pas vos pages avec cet lment encombrant, qui ne sert pas le jeu.

Le texte aussi
On a tendance penser que le texte a peu d'importance par rapport aux images, parce qu'il ne pse pas lourd et qu'on ne peut pas le compresser. Il ne mriterait pas qu'on se prenne la tte l'optimiser. Moi je dis : "Que nenni ! Dtrompez-vous !" Parce que, contrairement aux images, qui peuvent se mettre en cache sur le navigateur, le texte gnr par PHP est RECHARG ds qu'il y a un changement sur la page. Et puisqu'on cause de jeux en ligne, chaque changement de page il est cens y avoir rechargement des donnes. Ce qui signifie que la page de 30ko (et je reste modeste ; a monte vite avec quelques tableaux et une mise en page complexe) va tre charge, disons une quinzaine de fois par chaque joueur et que 1000x15x30ko par jour a fait beaucoup d'octets quand mme. Dans un jeu de plateau constitu de cases (type Nainwak), le navigateur doit charger par exemple 8x8 = 64 cases dotes chacune d'un fond d'cran et d'images, et ventuellement de liens et autres mouseover. Dans ce type de structure, le moindre caractre de texte est prcieux, puisqu'il sera envoy soixante quatre fois au navigateur du joueur chaque action.

Il y a des manires diverses de compresser le texte, dont l'efficacit dpend essentiellement du type de donnes prsentes sur la page. La premire proccupation, c'est de surveiller la taille des balises HTML. Utilisez les feuilles de style ! Il faut bannir les mises en forme de type <FONT> alors qu'il existe les <TD class="style"> (ou les <SPAN class="style"> si vous n'avez pas de tableau) bien plus conomiques. Il est trop facile de crer un code monumental partir d'une simple ligne PHP rpte 500 fois. Il est plus lgant de dfinir un style une fois pour toutes et de le rutiliser mille fois dans la page.

Autre ncessit : raccourcir les noms utiliss.
C'est vrai pour les feuilles de style : un class="d" sera plus conomique qu'un class="police_par_defaut_de_la_page_principale". Une lettre est souvent suffisante, ventuellement assortie de chiffres (d01,d02,d55...). Idem pour les images et leur rpertoire. Si vous deviez payer un euro par caractre, vous prfreriez copier 64 fois "images/image01.gif" ou "i/i01.gif" ? (ni l'un ni l'autre diront certains, mais vous avez compris l'ide : compresser ces caractres !). Pour les liens, on se retrouve dans la mme situation. Supposons que vous dsirez affecter une action chacune des 64 cases et que seul un numro varie. Le programmeur insouciant va copier gaillardement "action.php?action=deplacement&id=04" ou 05 ou 06 pour chaque case. La premire tape serait, vous l'aurez compris, d'en faire "ac.php?a=d&i=04", mais il y a encore moyen de faire mieux. Le programmeur cologiste va rduire cette ligne ce qu'elle contient d'essentiel : une action et un chiffre. Il pourra alors crer une fonction javascript qu'il nommera "d" (comme dplacement) ; le lien deviendra "d(04)" et la fonction se chargera de complter le reste de l'adresse de la page chaque appel.

Partir sur une bonne base
Vertiges de la base de donne ! Merveille que ces petites cases qui se remplissent et qui constituent le coeur de votre jeu. Jusqu' ce qu'elle sature ; qu'elle dborde ; et que la moindre recherche demande plusieurs minutes. L encore, il y a des rflexes importants pour ne pas gaspiller l'nergie du serveur.

Ne charger que le ncessaire
Une base de donnes permet de stocker la fois des donnes d'usage frquent et d'usage trs rare. Il est important de les dissocier, pour que, dans le droulement du jeu, seules les informations utiles soient recharges. Prenons le cas d'un jeu impliquant un personnage. Il sera important de dissocier les donnes du joueur (identifiant, mot de passe, e-mail) qui ne sont utiles qu' la connexion, et les donnes portant sur son personnage (points de vie, points d'action...) qui font l'objet d'un rechargement rgulier. L'idal, c'est de crer autant de bases qu'il y a de types de donnes dans le jeu : une base pour les objets, une pour les amis du joueur, que sais-je ? N'oubliez pas de faire correspondre chaque table, par exemple avec le mme numro d'identifiant pour le joueur et le personnage correspondant.

La mme question doit se poser pour de nombreux dtails. Est-il ncessaire de charger la liste complte des joueurs alors qu'il n'y en a que trois proximit ? Est-il ncessaire de charger la liste complte des objets transports par le joueur alors qu'on peut les "cacher" dans un sac dos qui s'ouvre dans ce but ?

Le choix pertinent des donnes afficher impose parfois des contorsions techniques (par exemple calculer les coordonnes minimales et maximales de visibilit d'un joueur pour afficher les personnages dont les coordonnes sont comprises dans cet intervalle). Au chapitre de l'arbitrage entre mySQL et PHP, il est parfois difficile de choisir le plus efficace. Un conseil : minimiser la charge PHP en faisant porter les calculs et le tris de donnes sur la base, qui est optimise pour. Il faut par exemple proscrire l'usage de "SELECT *" si seul un nom est ncessaire "SELECT nom" sera plus appropri. Il y a d'autres petits trucs pratiques. Si vous souhaitez afficher les 10 premiers enregistrements (par ex. les en-ttes des 10 derniers messages), pas la peine de charger toute la table en mmoire : "SELECT en_tete FROM jeu_messages LIMIT 10" permettra d'conomiser beaucoup de chargements puisque la recherche s'arrte ds que le 10e enregistrement est charg.

Le cas le plus critique est celui des messages : a enfle, a gonfle et a devient trs difficile compacter. Demandez-vous si la liste des messages doit tre charge systmatiquement en dbut de page. Une page ddie sera peut-tre approprie. Au besoin, il est possible de n'afficher que le dernier message, ou d'avertir le joueur qu'un message est arriv. Une requte "SELECT id FROM jeu_message LIMIT 1" permet de "dtecter" un nouveau message sans surcharger la base.

Compacter les donnes dans la base
On ne le rptera jamais assez : quand on n'a pas de ptrole, il faut avoir des ID (hum hum...). L'ID (pour index), c'est la cl de l'efficacit dans une base mySQL. Ce n'est pas facile admettre pour un dbutant, mais chaque joueur, chaque message, chaque objet du jeu ne doit tre reprsent que par un chiffre. Pourquoi ? Pour des raisons de rapidit et de libert. J'ai moi-mme dbut en faisant rfrence chaque personnage par son pseudonyme, qui me paraissait suffisamment unique pour tre reprsentatif. Mon code PHP comportait des tableaux de type $points["Roberto L'colo"]. Oui mais voil : tout d'abord j'ai eu de nombreux bugs cause des apostrophes et guillemets, notamment dans les requtes mysql. D'autre part le temps ncessaire pour une requte est bien plus important lorsqu'il s'agit de rechercher "Roberto L'colo" que pour rechercher "4055" dans un index. Cela tient la nature des donnes stockes dans la base.

Dans un champ "text" (ou "char" ou "blob"), chaque caractre est cod par 1 octet. Notre nom "Roberto L'colo" occupe donc 15 octets. Supposons qu'il soit dfini* par un index compris entre 0 et 65535 : cette valeur se code sur 3 octets. Cela signifie que, chaque fois que vous ferez rfrence ce personnage par son ID dans une requte ou un calcul PHP, la quantit de donnes calculer sera 5 fois plus petite qu'en utilisant son pseudo. Et croyez-moi, s'il y a 5000 joueurs sur votre partie, votre hbergeur vous remerciera de cet effort. * Cela ne signifie pas que le pseudo doive disparatre, mais il ne doit pas servir d'identifiant pour un joueur

Cette rduction s'applique aux autres donnes. Pour chaque valeur que vous souhaitez stocker dans la base, choisissez judicieusement le type de donnes. Comment faire ? Evaluez simplement la valeur MAXIMUM qui sera utile. Sans rentrer dans le dtail des types de donnes mySQL, voici quelques indications gnrales :

  • ID (identifiant de joueur) : MEDIUMINT UNIQUE -> permet de stocker 65536 joueurs avec un index automatique (en auto-increment, c'est mieux). Et si jamais vous atteignez les 65000 joueurs vous pouvez passer en INT (mySQL convertit sans problme les valeurs lorsqu'on augmente la longueur), ce qui vous autorisera 16 millions de joueurs, mais vous devriez le voir venir.
  • ID (autre identifiant) : s'il y a plusieurs parties dans votre jeu, utilisez par exemple un TINYINT (256 valeurs) pour les reprer. Pour les identifiants de messages, le type INT s'impose. Mieux vaut partir sur une valeur basse, quitte l'augmenter quand on constate que a ne va pas suffire.
  • Pseudonyme de personnage : c'est une valeur "lourde" qui pourrait tre place dans une table part si les accs la table des persos sont frquents ; par exemple dans la table des joueurs. Pour la longueur, prfrer un champ de type CHAR avec une longueur de 25 (a vitera les dlires de joueurs qui crent des noms de 500 caractres).
  • Valeur alternative oui/non ou 0/1 : SET avec pour valeur '0','1' ou 'oui','non' -> permet de dfinir les valeurs possibles pour un champ : chaque champ peut alors occuper 1 seul bit de donnes (un huitime d'octet) dans la base. Par comparaison un champ texte avec "oui" ou "non" occupe 24 fois plus d'espace.
  • Valeur numrique (points de vie, etc) : vous de dterminer le maximum acceptable, sans bloquer les joueurs puissants. En gnral, on optera pour du MEDIUMINT (65536).
  • Date : je n'ai pas de donnes prcises ce sujet : je prfre stocker les dates sous forme de timestamp (obtenue par un mktime() en PHP) dans un champ INT (a donne le nombre de secondes coules depuis le 1/1/1970, par exemple 1055190185), ce qui permet de manipuler les dates plus simplement qu'avec les fonctions DATE() de mySQL. Il est possible que je me plante sur ce sujet ; je reste ouvert aux suggestions.

    Enfin, si votre systme s'y prte, il peut tre intressant de multiplier les rfrences, mme si a impose une gestion trs rigoureuse. Dans Tratognse, on peut envoyer un mme message 10, 30, 40 personnes la fois. Au dpart, j'ai cr un message pour chaque destinataire. Si l'on compte l'ID de l'metteur, celle du rcepteur, le texte et la date d'mission du message, ma base est trs vite devenue astronomique (3 Mo de donnes pour "seulement" 12000 messages hebdomadaires). Puisque la seule donne qui changeait pour chaque rcepteur tait son ID, j'ai dcid de crer UN SEUL message de rfrence, pour 30 ou 40 messages qui "pointaient" vers ce message en ne comportant qu'un numro de rcepteur. Avec ce systme un peu compliqu, j'ai rduit la table 700ko, ce qui a considrablement acclr les accs la base. Ensuite, a complique un peu la rception et le "nettoyage" des messages trop vieux, mais on s'en sort.

    Optimiser ses requtes
    MySQL propose un certain nombre de fonctions qui acclrent grandement son usage : apprenez les connatre en consultant la documentation ! Supposons par exemple que vous souhaitiez connatre le nombre de joueurs dans votre table. Si vous ne connaissez que la syntaxe SELECT de base, vous pourriez arriver mysql_query("SELECT * FROM jeu_joueurs") puis utiliser mysql_affected_rows() pour connatre le nombre d'enregistrements retourns. C'est malin, sauf que vous avez charg l'intgralit de la table dans la mmoire, et qu'il existe une fonction spcialement optimise de mySQL pour connatre le nombre d'enregistrements : "SELECT COUNT(*) FROM jeu_joueurs". Au lieu de charger 800 lignes de donnes, vous ne chargez qu'un simple chiffre (rcuprable dans un tableau par un mysql_fetch_row(), mais ce n'est pas le sujet), d'o une conomie monumentale. Je vous invite, ds qu'un problme se pose, vrifier si la doc mySQL ne propose pas LA solution.

    Dans le mme ordre d'ides, vitez tout prix les requtes mySQL successives (par exemple dans une boucle for ou foreach portant sur chaque joueur). Chaque accs une base demande quelques millisecondes prcieuses, mme si la requte est rapide. Il vaut mieux donc trouver la bonne clause WHERE qui englobe tous les joueurs concerns que de rpter la mme action pour chacun, mais j'y reviendrai plus bas.

    Recycler son code
    Tout comme le tri des dchets mnagers, l'cologie du code est une astreinte pour le concepteur, mais un soulagement pour l'environnement (des joueurs). Il faut donc s'organiser et avoir une vision assez claire du "strict minimum" et du superflu. Et il y a plein d'endroits o le code, mine de rien, pourrait tre recycl, rutilis, compact. Quelques exemples :

    Diviser pour mieux ranger
    Les frames, c'est une chienlit. Pour la gestion des liens, pour la mmoire des sessions, c'est une torture. Pour rpondre ce problme, PHP permet de grer l'affichage conditionnel des lments d'une page. Par exemple, une structure IF ($inv== 1) print $inventaire autorise l'affichage des seuls lments dont on a besoin dans la page. Il est commode de tout runir dans une seule page, d'empiler les lignes de code dans un fichier bien dense o la logique apparat nettement. Pourtant, dans bien des cas, cela entrane des gchis. La solution s'appelle include(). En subdivisant votre code en petites pages ddies un affichage spcifique, le code ne sera charg par le serveur que si la condition du IF est remplie. Vous conservez la flexibilit des frames (on n'affiche que le ncessaire) tout en vitant leurs inconvnients.

    Vive les fonctions !
    La fonction, c'est le Graal du compactage. C'est vrai pour les fonctions spcifiques du code PHP : avant de taper dix lignes de code pour chercher les valeurs uniques dans un tableau, mieux vaut consulter la doc et constater qu'il existe array_unique(), spcialement ddie cet effet. C'est vrai galement des fonctions utilisateur, que vous crer pour automatiser une tche rptitive.

    Quand faut-il crer une fonction ? C'est simple, ds que vous utilisez une mme action deux fois de suite, avec quelques paramtres qui changent. Il faut tre attentif tous les endroits o l'on peut rduire le nombre de variables diffrentes et crer une fonction. Il peut tre trs conomique de crer des fonctions pour les requtes SQL. Ainsi, la fonction message (de qui, qui, texte) appellera la requte mySQL approprie, en calculant la date et tout ce qu'il faut.

    Au del de l'conomie, les fonctions assurent une cohrence au code (les messages seront toujours crits de la mme manire) et simplifient la mise jour (si l'on ajoute une colonne notre table de messages, il suffira de modifier la requte mySQL dans la fonction, et pas chaque appel).

    Vrifier AVANT de calculer
    Il y a des structures a priori innocentes, qui, prises dans le mauvais sens, peuvent mettre genoux un serveur. Supposons que vous vouliez vrifier pour chacun des 500 joueurs s'il a jou depuis 24h et, le cas chant, afficher son id. Il y a deux manires de procder :

    Le programmeur bte et obissant runit la liste des joueurs (il l'a dj obtenue dans un tableau $joueur[$id]). Il lance donc un foreach() qui va vrifier pour chaque joueur la date entre dans la base et la comparer la date actuelle. LE SERVEUR MEURT.

    Le programmeur cologiste, plus ft, a compris comment rationaliser cette situation. Il calcule la date moins 24h ($hier), puis excute une simple requte mysql_query ("SELECT id FROM jeu_persos WHERE date>$hier"). Il n'a plus qu'a lancer l'affichage pour chacun des rsultats. LE SERVEUR RIT.

    Dans le premier exemple, c'est la rptition de requtes minimes qui a surcharg le serveur : cela s'explique par le fait que toute requte, mme portant sur une seule entre, suppose un balayage d'une bonne partie de la table pour rapporter une seule info. Il vaut mieux rcuprer beaucoup d'infos une seule fois qu'une seule info 500 fois. Cette logique s'applique ds qu'une opration rptitive apparat dans le code. Tous les while, for, foreach doivent faire l'objet d'une enqute attentive : qu'est-ce qu'on peut sortir de la boucle ? Qu'est-ce qui peut tre calcul UNE SEULE FOIS au lieu de 500 fois ?

    Recycler ses emballages de variables
    A priori anecdotique, le nombre de variables utilises dans le code peut augmenter l'occupation de mmoire et ralentir d'autant le serveur. Ce nombre devrait tre rduit en rutilisant les "dchets". C'est surtout valable avec les appels $sql = mysql_query(), qui renvoient dans la variable $sql une ressource de taille souvent importante. Comme cette ressource est passe dans un mysql_fetch_array() ou mysql_fetch_row() pour tre utilisable, elle devient inutile et encombre donc la mmoire pour rien. Il y a deux solutions : si le rsultat de la requte ne fait qu'une ligne (par exemple le chargement des valeurs du joueur principal), vous pouvez rutiliser la mme variable pour en faire un tableau * : $sql = mysql_fetch_array($sql). Si le rsultat de la requte comporte plusieurs lignes, il ne peut pas tre rutilis directement puisqu'il faut crer un tableau et repasser la valeur pour chaque ligne. La solution : rutiliser la variable $sql dans les prochaines requtes SQL. Cette variable est donc usage multiple ; on garde son nom mais on efface son contenu pour le remplacer par un nouveau. * a peut paratre illogique que a=truc(a) mais rappelez vous que le signe "=" ne reprsente pas une galit mais une affectation : ds que la partie droite est calcule, la variable est cre avec la valeur calcule.

    Conclusion
    Il va de soi que cette prsentation n'est pas exhaustive. Ecolo-dveloppeur en herbe, je dcouvre chaque jour de nouvelles manires de simplifier, d'allger le jeu et d'conomiser les ressources limites du serveur. Il y a de vastes possibilits dans les fonctions mySQL (fonctions de recherches croises sur les tableaux, etc...) qui permettent des choses tonnantes dont je n'entrevois l'intrt qu'aprs deux ans de pratique quasi quotidienne du PHP. Mais je crois avoir abord l'lment essentiel qui s'impose ds que l'on veut allger un jeu : rechercher systmatiquement, frntiquement, les possibilits de compacter son jeu. Je l'ai dit, et je persiste : vos joueurs vous en seront reconnaissants... 

    Copyright TourDeJeu, portail des JpC, Pbem, et jeux en ligne alternatifs, site indépendant