aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Serveur de Jeu

Synchronisation d'état de jeu : la cohérence serveur

Dans un jeu multijoueur, si deux joueurs se trouvent dans la même pièce et que l'un voit l'ennemi droit devant tandis que l'autre le voit trois pas à gauche, le problème se situe presque toujours dans la couche de synchronisation d'état de jeu. L'« état du monde » partagé par le serveur et les clients diverge avec le temps, et cette divergence finit par devenir visible. Dans cet article, j'explique comment nous construisons la cohérence entre clients : le modèle de serveur autoritaire, les mises à jour par tick et l'interpolation, abordés de façon pratique.

Qu'est-ce que l'état, et pourquoi le synchroniser ?

Du point de vue d'un jeu, l'état est l'instantané du monde à un moment donné : positions, vitesses, points de vie et inventaires des joueurs, comportement des PNJ, portes ouvertes, objets tombés au sol. Dans un jeu solo, ce tableau vit dans une seule zone de mémoire et aucune incohérence n'est possible. Dans un jeu en réseau, en revanche, chaque client conserve sa propre copie de l'état et le serveur conserve la sienne. Comme la vitesse de la lumière est finie, ces copies ne peuvent jamais se mettre à jour au même instant ; notre objectif n'est pas d'éliminer l'incohérence mais de la garder assez petite et brève pour passer inaperçue.

La synchronisation résout deux questions centrales : qui fait foi comme « vérité », et comment remplir les images intermédiaires ? La première relève de l'autorité, la seconde de l'interpolation.

Le serveur autoritaire : une source unique de vérité

Le fondement de l'architecture multijoueur moderne est le modèle de serveur autoritaire. Ici, la source unique de vérité est le serveur. Lorsqu'un client appuie sur une touche, il ne dit pas « je suis à la position X en ce moment » ; il envoie une entrée (input) du type « je veux avancer ». Le serveur traite cette entrée dans sa propre simulation, calcule le résultat et rediffuse l'état mis à jour à tous les clients.

Le plus grand avantage de cette approche est la résistance à la triche. Le client ne peut pas écrire directement un état comme « les points de vie de l'ennemi sont à 0 » ou « je suis dans le mur » ; il ne fait qu'exprimer une intention, et c'est le serveur qui décide. Le même principe vaut pour les serveurs MMORPG comme Metin2 : les dégâts, les drops et la validation de position se font côté serveur, sinon un client modifiant sa propre mémoire pourrait tout casser.

  • Client → serveur : n'envoie que des entrées/commandes (direction, attaque, utiliser un objet).
  • Serveur : exécute la simulation, applique les collisions et les règles.
  • Serveur → clients : diffuse les nouveaux instantanés d'état.

Simulation par tick

Le serveur n'avance pas le monde en continu, mais à intervalles fixes. Chaque pas s'appelle un tick. Un serveur à 20 ticks/s met le monde à jour 20 fois par seconde (toutes les 50 ms). La raison d'un tick fixe est le déterminisme : lorsque les mêmes entrées sont traitées dans le même ordre avec le même pas de temps, le résultat est identique sur chaque machine.

// Boucle serveur à pas de temps fixe (simplifiée)
const double TICK_RATE = 20.0;            // ticks par seconde
const double DT = 1.0 / TICK_RATE;        // 0,05 s
double accumulator = 0.0;
double previous = now_seconds();

while (running) {
    double current = now_seconds();
    accumulator += current - previous;
    previous = current;

    while (accumulator >= DT) {
        process_inputs();   // appliquer les entrées des clients
        step_world(DT);     // physique + règles
        accumulator -= DT;
    }
    broadcast_snapshot();   // diffuser l'état aux clients
}

Le taux de tick est un équilibre. Un tick élevé (par ex. 60) donne un gameplay plus fluide et plus précis mais augmente le coût en bande passante et en CPU. Les jeux de tir nerveux exigent un tick élevé ; pour un MMO, 10 à 20 ticks suffisent généralement.

Instantanés, deltas et bande passante

Envoyer le monde entier à chaque client à chaque tick est du gaspillage. Il existe deux optimisations courantes :

  • Compression delta : n'envoyer que les champs modifiés par rapport à l'instantané précédent. Aucun flux de données pour un PNJ immobile.
  • Zone d'intérêt (area of interest) : n'envoyer à un joueur que les entités dans son champ de vision/d'influence. Inutile qu'il connaisse un combat à l'autre bout de la carte. À l'échelle d'un MMO, c'est indispensable.

Il faut aussi ne sérialiser que les champs pertinents pour le réseau (position, rotation, état d'animation, points de vie) plutôt que tous. Quantifier la position dans une plage bornée (par ex. 16 bits) au lieu de l'envoyer en pleine précision réduit considérablement les paquets.

Masquer la latence : interpolation et prédiction

Même si les instantanés n'arrivent que 20 fois par seconde, le joueur veut voir un mouvement fluide sur un écran 144 Hz. Deux techniques permettent de combler les trous.

Interpolation (pour les joueurs distants) : le client échantillonne en douceur la position entre les deux derniers instantanés reçus. Pour cela, il rend délibérément avec un petit décalage en arrière (par ex. 100 ms), de sorte qu'il dispose toujours de deux points de données réels entre lesquels effectuer la transition.

// Interpolation linéaire entre deux instantanés
Vec2 lerp(const Vec2& a, const Vec2& b, float t) {
    return { a.x + (b.x - a.x) * t,
             a.y + (b.y - a.y) * t };
}
// t : ratio du moment de rendu entre les deux temps d'instantané (0..1)

Prédiction côté client (pour votre propre joueur) : faire attendre votre propre personnage la réponse du serveur crée un input lag pénible. À la place, le client applique l'entrée localement au moment même où il l'envoie au serveur. Lorsque l'état officiel arrive du serveur, si la prédiction était correcte il n'y a aucune différence ; si elle était fausse, la réconciliation la corrige : le client revient à l'état confirmé par le serveur et rejoue les entrées non encore acquittées. En cas d'écart, on voit un petit « téléport » ; pour l'adoucir, la correction est fondue sur quelques images.

Sources d'incohérence et conseils pratiques

  • Perte de paquets : sur UDP, les instantanés peuvent se perdre. Utilisez un canal/mécanisme d'accusé de réception fiable pour les événements critiques (mort, ramassage d'objet) ; n'essayez pas d'envoyer de façon fiable des données continues comme la position, car l'instantané suivant apporte déjà des données fraîches.
  • Décalage d'horloge : les horloges du client et du serveur dérivent. Horodatez les instantanés avec un numéro de tick/timestamp du serveur, et interpolez selon ce temps plutôt que l'horloge murale.
  • Non-déterminisme des flottants : les résultats en virgule flottante peuvent varier légèrement selon les compilateurs/plateformes. Si vous avez besoin d'un déterminisme total, envisagez l'arithmétique en virgule fixe ; la plupart des serveurs autoritaires n'en ont pas besoin, car la source de vérité est unique.
  • Tests : injectez de la latence et des pertes de paquets artificielles pendant le développement (avec tc netem sous Linux). Un système agréable à 200 ms de latence sera impeccable à 20 ms.

Questions fréquentes

Dois-je utiliser TCP ou UDP ?

Pour les jeux nerveux et riches en positions, on préfère généralement UDP, car la garantie de livraison ordonnée de TCP fait attendre les nouveaux paquets pendant la retransmission d'un ancien paquet perdu (head-of-line blocking), ce qui gonfle la latence. Pour les jeux au tour par tour ou tolérants à la latence, TCP est largement suffisant. De nombreux moteurs construisent leur propre couche de fiabilité au-dessus d'UDP.

À quelle valeur régler le taux de tick ?

Cela dépend du gameplay. Dans les jeux de tir où le temps de réaction est critique, 30 à 64 ticks sont courants ; dans les MMO et les jeux de stratégie, 10 à 20 ticks suffisent et sont plus évolutifs. Décidez en mesurant d'abord la sensation de jeu, puis le coût serveur.

La prédiction côté client crée-t-elle un risque de triche ?

Non, car la prédiction n'est qu'une estimation visuelle ; la décision finale est toujours prise sur le serveur autoritaire. Si la prédiction locale du client entre en conflit avec le serveur, c'est l'état du serveur qui fait foi et le client est corrigé.

Vous construisez un serveur multijoueur ? Planifions ensemble l'architecture autoritaire, la conception des ticks et les stratégies de masquage de la latence, adaptées au genre de votre projet. Contactez-moi et discutons de vos besoins.

Bu kategorideki tüm yazılar →

Devamı için