Netcode : Victoire !

Récapitulons : Raydium est un moteur de jeu. Dans un moteur de jeu, on retrouve (entre autres) un moteur 3D, un moteur sonore, etc. mais aussi un moteur réseau, que l’on nomme vulgairement "netcode".
En général, on utilise un principe clients-serveur, ce qui signifie que chaque client possède une connexion à destination du serveur, et à l’inverse le serveur possède n connexions vers les différents clients. Pour faire une connexion réseau avec IP (pour que les connexions puissent être acheminées sur le net), on dispose de deux protocoles : TCP et UDP.

TCP est le plus courant sur le net (HTTP, FTP, SMTP, …), et on dit qu’il est "connecté", c’est à dire qu’il offre les notions de connexion/déconnection, et les données sont protégées par le protocole (toute perte de données est détectée, et corrigée grace à une réémission). C’est très efficace, mais c’est lent. Par exemple, la machine de destination doit accuser de la réception des données (ACK) et donc renvoyer des données en échange. L’autre problème en ce qui nous concerne (les jeux vidéo) c’est le goulot d’étranglement que risque de créer ce protocole : dans un jeu, on envoie beaucoup d’informations (positions des différents joueurs, par exemple), et TCP fera tout pour que la totalité des données soit bien reçue, même s’il doit mettre en attente la suite des données.

Or dans un jeu, la seule chose qui compte, c’est de connaitre la position des joueurs maintenant … et il est impensable d’attendre toutes les données (toutes les positions précédentes) pour connaitre la dernière position. Toute nouvelle donnée doit "périmer" les anciennes.
Le cas le plus simple pour démontrer ça, c’est d’imaginer un serveur disposant d’une bande passante dix fois supérieure à celle d’un client. Puisque le client est lent, le serveur va devoir mettre en attente les nouvelles données… et le client va recevoir l’information (les positions) de plus en plus "en retard" (lag).

Donc on en arrive à UDP : il est lui complétement déconnecté, son seul but est d’essayer d’envoyer des données. Pas de détection d’erreurs (paquets perdus), pas de notion de connexion (n’importe qui peut envoyer des données sur le port en attente du serveur, sans négociation préalable), ni de paquet (si l’information est trop grosse, à vous de la découper et de vous débrouiller pour que tout arrive dans le bon ordre), etc. (Attention, contrairement à la légende, UDP possède bien un petit contrôle d’intégrité, basé sur une CRC). Autre aspect : pas de "bourrage" possible. Si de nouvelles données arrivent et qu’il n’y a plus de place (bande passante trop faible), les nouvelles données écrasent les anciennes.
C’est l’anarchie, mais c’est beaucoup (beaucoup) plus rapide que TCP.

UDP est donc très utilisé pour les applications dans lequelles les données ne sont "pas trop importantes" (par rapport à la vitesse) : streaming vidéo et jeux vidéo, par exemple. (Certains seront peut être étonnés de savoir que DNS utilise uniquement de l’UDP).

Oui mais voilà, dans un jeu vidéo, il y’a certes des données "pas importantes" (positions des joueurs, encore une fois), mais aussi des données "importantes" : explosion d’une roquette à tel endroit, informations sur le joueur (équipe, couleur, score, …) qu’il est hors de question de rater.

Il y’a deux solutions à ce problème (notez qu’il concerne la totalité des jeux dits "temps réel"). La première consiste à ouvrir deux canaux vers le serveur : l’un en TCP (truc importants), et l’autre en UDP (trucs pas important). Cette solution semble très intéressante, mais cache en fait de nombreux problèmes d’implémentation :
– énorme complexité de la synchro entre les deux canaux
– poids de TCP sur UDP (TCP a tendance à être prioritaire sur UDP)
– protocole plus complexe (deux ports au lieu d’un : firewalls, routage, statistiques, …)

L’autre solution est plus longue, mais beaucoup plus souple à terme : reprogrammer certains comportements de TCP au dessus d’UDP :
– sécurité du transfert (données perdues détectées et ré-émises)
– notion de connexion ("le joueur ‘truc’ s’est déconnecté")
– timeout
– rang (le paquet 10 doit arriver avant le 11)
– …

Résultat, on n’active ces systèmes que pour les paquets importants, et on utilise l’UDP "de base" pour le reste.

Quand j’ai commençé ce travail pour Raydium, je savais bien que ça allait être long, dur et pleins de pièges … eh bien c’est l’une des seules fois ou je me suis trompé sur l’ampleur du travail : c’est un enfer, un cauchemard ! 🙂
Coder une couche réseau (de bas niveau, qui plus est), ça signifie que le débogage est quasi impossible, que le comportement de la couche réseau change du tout au tout en fonction de l’environnement (nombre de clients, débit total, débit de chaque client, …) sans que l’on sache vraimment pourquoi, etc. Ajoutez la gestion d’un moteur physique sur le réseau, et vous avez de quoi rendre malade un codeur sur 5 générations.

M’enfin puisque c’est quand même vachement intéressant, petit à petit, la couche réseau de Raydium a avancé, corrigée bug par bug, fonctionnalité par fonctionnalité, pour enfin atteindre un stade intéressant : elle marchait en réseau local ! (le tout à du s’étaler sur deux ans, facilement).
Par contre, sur le net, les performances étaient médiocres, et tout s’écroulait (déco pour tout le monde) dès qu’un client LAN se connectait.

Le problème en lui même était simple, l’explication est plus complexe. En référence à ce qui est dit plus haut, la fonctionnalité la plus importante que doit offrir notre couche réseau basée sur UDP est la détection de paquets perdus. Pour détecter si un paquet est perdu, il faut mettre en place un système d’accusés de réception, et il faut se donner un délais maximal de retour de l’accusé de réception au delà duquel on considère le paquet comme perdu. Concrétement, ce délais (timeout) est le double de la moyenne pondérée du temps de retour des accusés de réception, doublée (la moyenne) pour chaque paquet perdu (c’est tout à fait le principe utilisé par les piles TCP).
Après pas mal de tests (et un peu de réflexion, aussi), nous nous sommes aperçus que les serveurs Raydium n’avait qu’un seul de ces délais, au lieu d’en avoir un pour chaque client connecté. Les clients connectés au travers d’un réseau local avait donc tendance à réduire très fortement le délais, "stressant" les clients connectés sur le net, qui se faisaient donc engueuler parcequ’il n’envoyaient pas les accusés de réception assez vite, engueulade qui consomait encore plus de bande passante, … le cercle vicieux.
Après ré-écriture de cette partie du netcode, nous avons organisé un test. 3 joueurs, dont 2 sur le net (lignes ADSL), un client sur un réseau local (54 mbps), et le serveur (ligne ADSL 8/1 mbps), et pour la première fois, nous avons réussi à jouer à notre mini FPS dans ces conditions !

Du premier coup (fait rare), sans le moindre plantage, et avec un lag tout à fait acceptable !
Ajouté à la prédiction de trajectoires, à la physique réseau, la couche UDP de Raydium nous permet d’avoir un netcode (enfin) complet et fonctionnel sur le net 🙂

Référence sur le forum de Raydium : http://memak.raydium.org/viewtopic.php?t=175#1366


Publié

dans

par

Étiquettes :

Commentaires

5 réponses à “Netcode : Victoire !”

  1. Avatar de pololefou
    pololefou

    super interessant ton article et comprehensible qui plus est…
    je me coucherais moins con ce soir!! merci xfennec!!

  2. Avatar de eastwitch
    eastwitch

    très intéressant. Continuez comme ça !

  3. Avatar de Xfennec
    Xfennec

    Merci à vous deux 😉 (ça fait toujours plaisir de voir que y’a des gens qui lisent 🙂

  4. Avatar de CopSuck
    CopSuck

    Je confirme : j’ai lu.
    Sinon j’aimerais, si c’est possible une petite explication, à propos des gros serveurs de jeux (64 à 128 joueurs) : les difficultés etc.
    En même temps si vous ne vous orientez pas la-dedans pour votre jeu ne te sends pas obliger de le faire.

  5. Avatar de feneki
    feneki

    J’ai lu et c’est réellement super édifiant comme article. J’ai fait un tout tout petit peu de programmation des systèmes répartis et concurrents et j’ai fait un peu de TCP, d’UDP de bas niveau. Je ne suis pas un codeur mais j’ai compris les intérêts des différents protocoles.

    Et c’est tellement clair, limpide même.
    Merci XFennec, tu fais un job d’enfer.

Laisser un commentaire