01 /

Charger une zone sans figer le monde.

Quand un joueur entre dans un chunk, mon serveur prépare un snapshot : une photographie des objets visibles pour son instance, accompagnée d’une version. Mais le jeu continue pendant le transfert. Un objet peut être déplacé, modifié ou supprimé.

J’envoie ces changements sous forme de deltas. La difficulté se situe à leur arrivée : si je remplace simplement le cache par le snapshot, je risque d’écraser une modification plus récente. J’ai pris le cas d’une suppression pour illustrer le mécanisme : un objet supprimé pendant le chargement ne doit pas revenir avec les anciennes données.

02 / Le snapshot et la suppression1 / 5
Serveur · état courant
ABC
↑ Demande de chunk
Remplacement naïf
ABC

Le snapshot est attendu.

Réconciliation du streamer
ABC

En attente

Un chunk est demandé.

Le client entre dans une zone. Le serveur prépare une photographie du chunk à la version 10.

Scénario pédagogique à trois objets. L’ordre choisi illustre une suppression plus récente que le snapshot ; les étapes ne représentent pas des durées mesurées ni l’intégralité du protocole.
02 /

Répartir les envois entre les clients.

J’ai découpé les snapshots avec deux seuils : 48 objets par partie et environ 48 Kio de contenu estimé. J’évalue le volume avec JSON et une marge. Un objet isolé plus gros que ce seuil peut occuper sa propre partie ; cette estimation sert à répartir le travail, elle ne fixe pas la taille exacte du paquet réseau.

Après les métadonnées, je place les parties dans des files par destinataire. Mon worker se réveille toutes les 80 ms et envoie au plus quatre parties par passage. Il évite de servir deux fois le même client pendant un passage, pour répartir les envois entre les destinataires en attente.

J’utilise des événements latents avec un débit configuré de 350 000 octets par seconde. Cela me permet de régler le transfert des parties volumineuses. Pour un snapshot vide, j’ai prévu un chemin court qui évite ce découpage.

03 /

Reconstruire à côté du cache actif.

Je construis le prochain chunk dans une structure temporaire. Une passe qui cède la main n’expose donc pas un cache à moitié reconstruit. J’associe ce travail à une génération et à une époque pour pouvoir abandonner une reconstruction devenue obsolète.

Pendant cette reconstruction, je conserve les mutations entrantes par identifiant. Je mémorise également la version des suppressions : si un objet a été supprimé à une version plus récente que celle du snapshot, je l’écarte. Je réconcilie ensuite les mutations en attente avec les données reçues.

J’ai aussi coordonné l’application des snapshots avec celle des batches de deltas. Si un batch est déjà en cours, je peux différer le snapshot. Ces garde-fous restent nécessaires lorsque le traitement s’étale sur plusieurs frames.

PSEUDO-CODE EXPLICATIFsi snapshot.version < cache.version : ignorer
construire le prochain chunk hors du cache actif
écarter les suppressions plus récentes
réconcilier les mutations reçues entre-temps
valider la génération avant de poursuivre
04 /

Donner un sens aux versions.

Je regroupe les deltas par chunk et par destinataire. Plusieurs modifications d’un même objet peuvent être fusionnées avant l’envoi, ce qui signifie que le client ne reçoit pas nécessairement toutes les versions intermédiaires.

Mon traitement distingue les deltas unitaires des batches. Pour un delta unitaire, un saut de version déclenche une demande de resynchronisation après application. Pour un batch, j’accepte les sauts comme une conséquence possible du regroupement ; ce chemin ne lance pas la même demande à cet endroit.

Je m’appuie sur les versions pour rejeter des états obsolètes et ordonner la réconciliation. Elles ne suffisent toutefois pas à établir que toutes les mutations ont été reçues : les décisions de reprise dépendent aussi du chemin de transport et de l’état du chunk.

Le principe

Je traite le snapshot comme un point de départ. Les mutations reçues pendant son chargement doivent participer à l’état final.

Poursuivre l’exploration

Comment je répartis le budget du streamer.