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

Game Loop et Tick Rate : Concevoir la Boucle Serveur

Une seule chose bat au cœur de chaque serveur de jeu : la boucle. Le game loop tick rate détermine combien de fois par seconde le serveur met à jour le monde — calcul de la physique, application des déplacements et envoi de l'état aux clients. Si ce nombre, et la façon dont la boucle est construite, sont mal choisis, les joueurs subissent des personnages qui se téléportent, des collisions incohérentes et un gameplay qui s'effondre dès que le rythme s'accélère. Dans cet article, j'explique ce qu'est un tick, pourquoi un pas de temps fixe est nécessaire et comment construire une boucle serveur stable en C++.

Qu'est-ce qu'un tick, et que signifie le tick rate ?

Un tick est un pas unique d'avancement de la simulation. À chaque tick, le serveur fait avancer l'état du jeu d'une quantité de temps fixe : il lit les entrées, déplace les personnages, résout les collisions et calcule le résultat. Le tick rate est le nombre de ticks par seconde, généralement exprimé en Hz. 20 ticks/s signifie que le monde est mis à jour 20 fois par seconde (une fois toutes les 50 ms).

Les valeurs courantes varient selon le genre :

  • 10–20 Hz — MMO et RPG non orientés tir, où le temps de réaction n'est pas critique.
  • 30 Hz — un bon compromis pour de nombreux jeux d'action et de survie.
  • 60–128 Hz — FPS compétitifs, où la visée et l'enregistrement des impacts exigent une grande précision.

Augmenter le tick rate rend le jeu plus réactif, mais accroît proportionnellement la charge CPU et la bande passante. 64 Hz, c'est deux fois plus de travail — et généralement deux fois plus de paquets — que 32 Hz. Le bon nombre est l'équilibre entre la précision que votre jeu exige et la charge que votre serveur peut supporter.

Pourquoi un pas de temps fixe ?

Une boucle naïve s'écrit souvent ainsi : « mesurer le temps écoulé, faire avancer tout d'autant ». C'est un pas de temps variable, et c'est source de problèmes. Quand delta change à chaque image, la physique n'est pas déterministe : une même entrée peut produire deux résultats différents, les objets rapides peuvent traverser les collisions (tunneling), et le serveur et le client ne trouvent jamais le même nombre.

Un pas de temps fixe règle cela. On fait toujours avancer la simulation du même dt (par exemple 1/30 de seconde). Même si le temps réel s'écoule plus vite ou plus lentement, la logique progresse de la même quantité à chaque pas. Ce déterminisme est essentiel pour les replays, la détection de triche et une architecture où le serveur fait autorité.

La logique repose sur un accumulateur : on ajoute le temps réel écoulé à un seau, et tant que le seau dépasse un dt, on exécute des pas fixes. Si le CPU prend une image de retard, la boucle exécute plusieurs ticks d'affilée pour vider l'accumulateur (« rattrapage »), de sorte que le temps de simulation ne reste jamais en retard sur le temps réel.

Une boucle serveur à pas fixe

L'exemple C++ ci-dessous montre la boucle classique fondée sur un accumulateur, avec une horloge haute résolution via std::chrono :

#include <chrono>
#include <thread>

using clock_t = std::chrono::steady_clock;
using namespace std::chrono;

const int   TICK_RATE = 30;
const double DT = 1.0 / TICK_RATE;       // pas fixe, en secondes

void run_server() {
    auto previous = clock_t::now();
    double accumulator = 0.0;

    while (server_running) {
        auto now = clock_t::now();
        double frame = duration<double>(now - previous).count();
        previous = now;

        // Une image lente ne doit pas déclencher la spiral of death
        if (frame > 0.25) frame = 0.25;
        accumulator += frame;

        while (accumulator >= DT) {
            process_input();        // appliquer les paquets clients
            update_world(DT);       // physique + logique, toujours le même DT
            accumulator -= DT;
        }

        broadcast_state();          // envoyer l'état courant aux clients

        // Dormir jusqu'au prochain tick pour ne pas faire tourner le CPU à vide
        auto next = previous + duration_cast<clock_t::duration>(duration<double>(DT));
        std::this_thread::sleep_until(next);
    }
}

Trois points critiques ici : DT est toujours constant (pas le temps écoulé variable de l'horloge), le temps d'image est plafonné (sinon, si le serveur se bloque, l'accumulateur enfle et la boucle se fige dans des ticks sans fin — la « spiral of death »), et sleep_until attend le prochain tick pour que le CPU ne brûle pas 100 % à tourner à vide.

Précision du sommeil et dérive temporelle

Utiliser sleep_until plutôt que sleep_for a son importance. sleep_for(33ms) dit « dors 33 ms » à chaque fois, mais la latence de réveil s'accumule à chaque tour et décale lentement les ticks. sleep_until(next) vise un instant cible absolu ; si un tick se réveille en retard, le suivant se réveille plus tôt et maintient la moyenne.

La résolution du sommeil de l'OS n'est pas parfaite : quelques centaines de microsecondes sur Linux, et des millisecondes sur Windows avec le minuteur par défaut. Si vous visez un tick rate élevé (64+ Hz), une technique courante est un court busy-wait juste avant l'instant cible pour affiner l'horloge — mais cela occupe un cœur CPU, à utiliser avec parcimonie là où cela compte.

Tick rate, débit réseau et côté client

Le rythme de simulation du serveur et son rythme d'envoi de l'état sur le réseau ne sont pas forcément identiques. Un serveur peut simuler à 60 Hz mais ne diffuser l'état qu'à 20 Hz, ce qui réduit la bande passante tout en préservant la qualité de simulation. Le client comble l'écart par interpolation (lissage entre deux états passés) et prédiction pour ses propres entrées.

Une checklist pratique :

  • Choisissez le rythme de simulation selon la précision dont votre jeu a besoin ; pensez le débit réseau séparément.
  • Gardez le serveur comme autorité : le client envoie l'entrée, le serveur calcule le résultat.
  • Mettez un numéro de tick dans chaque paquet d'état pour que le client sache quel pas il voit.
  • Mesurez la durée d'un tick ; si elle commence à dépasser DT, baissez le tick rate ou optimisez le travail.

Erreurs fréquentes

  • Physique sur un delta variable : casse le déterminisme ; l'enregistrement des impacts et les replays deviennent peu fiables.
  • Accumulateur sans plafond : dès que le serveur se bloque, la spiral of death le verrouille totalement.
  • E/S bloquantes dans le tick : un appel disque ou base de données synchrone fige toute la simulation ; déportez-les sur un thread ou une file séparés.
  • Augmenter le tick rate à l'aveugle : CPU et bande passante croissent linéairement ; mesurez d'abord le goulot d'étranglement.

Questions fréquentes

Quel tick rate choisir ?

Cela dépend du genre. 10–20 Hz suffisent pour un MMO au rythme lent ; 30 Hz est un point de départ équilibré pour les jeux d'action ; les shooters compétitifs visent 60–128 Hz. Déterminez d'abord le temps de réaction dont votre jeu a besoin, puis mesurez si le serveur peut le supporter.

Quelle est la différence entre pas de temps fixe et variable ?

Avec un pas fixe, la simulation avance toujours du même dt et est déterministe ; avec un pas variable, chaque image utilise le temps réel écoulé, ce qui rend la physique incohérente et non reproductible. Les jeux où le serveur fait autorité utilisent presque toujours un pas fixe.

Si j'augmente le tick rate, la latence baisse-t-elle ?

Un peu : un tick rate plus élevé raccourcit l'attente entre les traitements d'entrées par le serveur. Mais cela ne change pas le temps d'aller-retour réseau (ping). La latence ressentie dépend surtout du réseau et de la conception de l'interpolation/prédiction côté client.

Votre boucle serveur est instable, ou vous cherchez le bon tick rate ? Passons ensemble en revue l'architecture de votre serveur de jeu et construisons une game loop stable et déterministe — contactez-moi.

Bu kategorideki tüm yazılar →

Devamı için