Comment un système d'instantanés d'état du jeu serait-il implémenté pour les jeux en temps réel en réseau?

12

Je veux créer un simple jeu multijoueur client-serveur en temps réel comme projet pour ma classe de réseautage.

J'ai beaucoup lu sur les modèles de réseaux multijoueurs en temps réel et je comprends les relations entre le client et le serveur et les techniques de compensation de décalage.

Ce que je veux faire, c'est quelque chose de similaire au modèle de réseau Quake 3: fondamentalement, le serveur stocke un instantané de tout l'état du jeu; lors de la réception des entrées des clients, le serveur crée un nouvel instantané reflétant les changements. Ensuite, il calcule les différences entre le nouvel instantané et le dernier et les envoie aux clients, afin qu'ils puissent être synchronisés.

Cette approche me semble vraiment solide - si le client et le serveur ont une connexion stable, seule la quantité minimale nécessaire de données sera envoyée pour les synchroniser. Si le client n'est plus synchronisé, un instantané complet peut également être demandé.

Je ne peux cependant pas trouver un bon moyen de mettre en œuvre le système d'instantanés. Je trouve vraiment difficile de m'éloigner de l'architecture de programmation solo et de réfléchir à la façon dont je pourrais stocker l'état du jeu de telle manière que:

  • Toutes les données sont séparées de la logique
  • Les différences peuvent être calculées entre un instantané des états du jeu
  • Les entités de jeu peuvent toujours être facilement manipulées via le code

Comment une classe d' instantanés est -elle implémentée? Comment les entités et leurs données sont-elles stockées? Chaque entité cliente a-t-elle un ID qui correspond à un ID sur le serveur?

Comment les différences d'instantanés sont-elles calculées?

En général: comment un système d'instantanés d'état du jeu serait-il implémenté?

Vittorio Romeo
la source
4
+1. C'est un peu trop large pour une seule question, mais l'OMI est un sujet intéressant qui peut être à peu près couvert dans une réponse.
Kromster
Pourquoi ne pas simplement stocker 1 instantané (le monde réel), enregistrer toutes les modifications entrantes dans cet état mondial normal ET stocker les modifications dans une liste ou quelque chose. Ensuite, lorsqu'il est temps d'envoyer les modifications à tous les clients, il suffit d'envoyer le contenu de la liste à tous et d'effacer la liste, commencez à zéro (modifications). Peut-être que ce n'est pas aussi bon que de stocker 2 instantanés, mais avec cette approche, vous n'avez pas à vous soucier des algorithmes sur la façon d'accélérer la différence de 2 instantanés.
tkausl
Avez-vous lu ceci: fabiensanglard.net/quake3/network.php - l'examen du modèle de réseau de tremblement de terre 3 comprend une discussion sur la mise en œuvre.
Steven
Quel genre de jeu tente de construire? La configuration du réseau dépend fortement du type de jeu que vous créez. Un RTS ne se comporte pas comme un FPS en termes de mise en réseau.
AturSams

Réponses:

3

Vous pouvez calculer le delta de l'instantané (modification de son état synchronisé précédent) en conservant deux instances d'instantanés: une actuelle et la dernière synchronisée.

Lorsque l'entrée client arrive, vous modifiez l'instantané actuel. Ensuite, quand il est temps d'envoyer du delta aux clients, vous calculez le dernier instantané synchronisé avec un champ actuel par champ (récursivement) et calculez et sérialisez le delta. Pour la sérialisation, vous pouvez attribuer un ID unique à chaque champ de l'étendue de sa classe (par opposition à l'étendue de l'état global). Le client et le serveur doivent partager la même structure de données pour l'état global afin que le client comprenne à quoi un ID particulier est appliqué.

Ensuite, lorsque le delta est calculé, vous clonez l'état actuel et en faites le dernier état synchronisé, vous avez donc maintenant un état actuel et un état synchronisé identiques, mais des instances différentes afin de pouvoir modifier l'état actuel et ne pas affecter l'autre.

Cette approche peut être plus facile à mettre en œuvre, en particulier avec l'aide de la réflexion (si vous avez un tel luxe), mais peut être lente, même si vous opitimisez fortement la partie réflexion (en construisant votre schéma de données pour mettre en cache la plupart des appels de réflexion). Principalement parce que vous devez comparer deux copies d'un état potentiellement volumineux. Bien sûr, cela dépend de la façon dont vous implémentez la comparaison et de votre langue. Il peut être rapide en C ++ avec un comparateur codé en dur mais pas si flexible: tout changement de votre structure d'état globale nécessite une modification de ce comparateur, et ces changements sont si fréquents lors des premières étapes du projet.

Une autre approche consiste à utiliser des drapeaux sales. Chaque fois qu'une entrée client arrive, vous l'appliquez à votre copie unique de l'état global et marquez les champs correspondants comme sales. Ensuite, quand il est temps de synchroniser les clients, vous sérialisez (récursivement) les champs sales en utilisant les mêmes ID uniques. L'inconvénient (mineur) est que, parfois, vous envoyez plus de données que strictement requis: par exemple, int field1était initialement 0, puis affecté 1 (et marqué sale) et ensuite attribué à nouveau 0 (mais reste sale). L'avantage est qu'avec une énorme structure de données hiérarchique, vous n'avez pas besoin de l'analyser complètement pour calculer le delta, uniquement des chemins sales.

En général, cette tâche peut être assez compliquée, dépend de la flexibilité de la solution finale. Par exemple, Unity3D 5 (à venir) utilisera des attributs pour spécifier les données qui doivent être auto-synchronisées avec les clients (approche très flexible, vous n'avez rien d'autre à faire que d'ajouter un attribut à vos champs), puis générer du code en tant que étape post-construction. Plus de détails ici.

Andriy Tylychko
la source
2

Vous devez d'abord savoir comment représenter vos données pertinentes d'une manière conforme au protocole. Cela dépend des données pertinentes pour le jeu. Je vais utiliser un jeu RTS comme exemple.

À des fins de réseautage, toutes les entités du jeu sont énumérées (par exemple, les micros, les unités, les bâtiments, les ressources naturelles, les destructibles).

Les joueurs doivent avoir les données qui les concernent (par exemple toutes les unités visibles):

  • Sont-ils vivants ou morts?
  • De quel type s'agit-il?
  • Combien de santé leur reste-t-il?
  • Position actuelle, rotation, vitesse (vitesse + direction), trajectoire dans un futur proche ...
  • Activité: Attaquer, marcher, construire, réparer, guérir, etc ...
  • effets d'état de buff / debuff
  • et peut-être d'autres statistiques comme le mana, les boucliers et quoi d'autre?

Au début, le joueur doit recevoir l'état complet avant de pouvoir entrer dans le jeu (ou alternativement toutes les informations pertinentes pour ce joueur).

Chaque unité a un ID entier. Les attributs sont énumérés et ont donc également des identificateurs intégraux. Les identifiants des unités ne doivent pas nécessairement être de 32 bits (ce pourrait être le cas si nous ne sommes pas frugaux). Il pourrait très bien être de 20 bits (laissant 10 bits pour les attributs). L'ID des unités doit être unique, il pourrait très bien être attribué par un compteur lorsque l'unité est instanciée et / ou ajoutée au monde du jeu (les bâtiments et les ressources sont considérés comme une unité immobile et les ressources peuvent se voir attribuer un ID lorsque la carte est chargé).

Le serveur stocke l'état global actuel. L'état le plus récent mis à jour de chaque joueur est représenté par un pointeur sur l'un listdes changements récents (tous les changements après que le pointeur n'a pas encore été envoyé à ce joueur). Les modifications sont ajoutées au listmoment où elles se produisent. Une fois que le serveur a terminé d'envoyer la dernière mise à jour, il peut commencer à parcourir la liste: le serveur déplace le pointeur du joueur le long de la liste jusqu'à sa queue, collecte toutes les modifications en cours de route et les place dans un tampon qui sera envoyé à le joueur (c'est-à-dire que le format du protocole peut être quelque chose comme ceci: unit_id; attr_id; new_value) Les nouvelles unités sont également considérées comme des changements et sont envoyées avec toutes leurs valeurs d'attribut aux joueurs récepteurs.

Si vous n'utilisez pas une langue avec un garbage collector, vous devrez configurer un pointeur paresseux qui sera en retard, puis rattraper le pointeur de lecteur le plus obsolète de la liste, libérant des objets en cours de route. Vous pouvez vous rappeler quel joueur est le plus obsolète dans un tas prioritaire ou simplement itérer et libérer jusqu'à ce que le pointeur paresseux soit égal (c'est-à-dire qu'il pointe vers le même élément que l'un des pointeurs des joueurs).

Certaines questions que vous n'avez pas soulevées et qui me semblent intéressantes sont les suivantes:

  1. Les clients doivent-ils recevoir un instantané avec toutes les données en premier lieu? Qu'en est-il des objets en dehors de leur champ de vision? Qu'en est-il du brouillard de guerre dans les jeux RTS? Si vous envoyez toutes les données, le client pourrait être piraté pour afficher des données qui ne devraient pas être disponibles pour le joueur (en fonction des autres mesures de sécurité que vous prenez). Si vous envoyez uniquement des données pertinentes, le problème est résolu.
  2. Quand est-il essentiel d'envoyer des modifications au lieu d'envoyer toutes les informations? Compte tenu de la bande passante disponible sur les machines modernes, gagnons-nous quelque chose à envoyer un "delta" au lieu d'envoyer toutes les informations, si oui quand?
AturSams
la source