Comment garder une structure de données synchronisée sur un réseau?

12

Le contexte

Dans le jeu sur lequel je travaille (une sorte d'aventure graphique pointer-cliquer), à peu près tout ce qui se passe dans le monde du jeu est contrôlé par un gestionnaire d'action qui est structuré un peu comme:

entrez la description de l'image ici

Ainsi, par exemple, si le résultat de l'examen d'un objet doit faire dire bonjour au personnage, marcher un peu puis s'asseoir, je lance simplement le code suivant:

var actionGroup = actionManager.CreateGroup();
actionGroup.Add(new TalkAction("Guybrush", "Hello there!");
actionGroup.Add(new WalkAction("Guybrush", new Vector2(300, 300));
actionGroup.Add(new SetAnimationAction("Guybrush", "Sit"));

Cela crée un nouveau groupe d'actions (une ligne entière dans l'image ci-dessus) et l'ajoute au gestionnaire. Tous les groupes sont exécutés en parallèle, mais les actions au sein de chaque groupe sont enchaînées de sorte que le second ne démarre qu'après la fin du premier. Lorsque la dernière action d'un groupe se termine, le groupe est détruit.

Problème

Maintenant, je dois répliquer ces informations sur un réseau, de sorte que dans une session multijoueur, tous les joueurs voient la même chose. La sérialisation des actions individuelles n'est pas le problème. Mais je suis un débutant absolu en matière de réseautage et j'ai quelques questions. Je pense que pour des raisons de simplicité dans cette discussion, nous pouvons résumer le composant gestionnaire d'actions comme étant simplement:

var actionManager = new List<List<string>>();

Comment dois-je procéder pour garder le contenu de la structure de données ci-dessus synchronisé entre tous les joueurs?

Outre la question centrale, j'ai également quelques autres préoccupations qui y sont liées (c'est-à-dire toutes les implications possibles du même problème ci-dessus):

  • Si j'utilise une architecture serveur / client (avec l'un des joueurs agissant à la fois comme serveur et comme client), et que l'un des clients a généré un groupe d'actions, devrait-il les ajouter directement au gestionnaire ou envoyer uniquement une demande au serveur, qui à son tour ordonnera à chaque client d'ajouter ce groupe ?
  • Qu'en est-il des pertes de paquets et similaires? Le jeu est déterministe, mais je pense que toute divergence dans la séquence des actions exécutées chez un client pourrait conduire à des états du monde incohérents. Comment puis-je me prémunir contre ce genre de problème ?
  • Et si j'ajoute trop d'actions à la fois, cela ne causera-t-il pas des problèmes de connexion? Y a-t-il un moyen d'atténuer cela?
David Gouveia
la source
5
Lien "lire ce premier" obligatoire gafferongames.com/networking-for-game-programmers qui n'est pas très technique malgré un code mais qui est verbeux et explique assez bien vos options. De plus, cela vous mettra sur un pied d'égalité avec tous ceux qui ont lu ce lien et c'est un endroit heureux.
Patrick Hughes
@PatrickHughes Merci pour le lien! Je vais certainement tout lire.
David Gouveia
Il existe des packages qui gèrent ce genre de choses pour vous. Je ne sais pas à quel point c'est rapide ou efficace, mais NowJS ( nowjs.com ) est un exemple intéressant. Du point de vue des développeurs, vous disposez simplement de structures de données partagées entre le client et le serveur. Changez l'un, l'autre change.
Tim Holt

Réponses:

9

Si j'utilise une architecture serveur / client (avec l'un des joueurs agissant à la fois comme serveur et comme client) et que l'un des clients a généré un groupe d'actions, devrait-il les ajouter directement au gestionnaire ou envoyer uniquement une demande au serveur, qui à son tour ordonnera à chaque client d'ajouter ce groupe?

Vous avez trois options dans ce cas, dont deux que vous avez déjà mentionnées. Le choix de l'option que vous choisissez dépend d'une variété de facteurs dont vous seul pouvez être le meilleur juge:

1: le client génère un groupe d'actions qui les ajoute directement, puis les envoie au serveur. Le serveur l'accepte sans aucun contrôle

* Les avantages de cette approche sont qu'en ce qui concerne le client, ses propres actions lui donnent un retour instantané indépendant du retard du réseau. L'inconvénient est que les messages du client sont acceptés par le serveur comme des faits (ce qu'ils peuvent ne pas être) *

2: Le client envoie une demande au serveur, qui à son tour diffuse la demande à tout le monde, y compris le client à l'origine de la demande.

Les avantages de cette approche sont que le serveur contrôle désormais tout ce qui se passe dans le jeu, ce qui atténue la plupart des problèmes d'activité illégale (ici, l'illégal peut aller du client qui coupe un mur à effectuer un mouvement qui est maintenant illégal car le serveur a plus d'informations que le client n'avait pas lorsque le client a fait un mouvement particulier)

3: Le client génère un groupe d'actions, les ajoute directement puis les envoie au serveur. Le serveur traite la demande, exécute ses propres vérifications sur les actions pour s'assurer qu'elles ne sont pas illégales, puis diffuse le mouvement à tous les joueurs, y compris le client.

Cela prend le meilleur de 1 et 2 au prix d'exigences de bande passante légèrement plus élevées. Les avantages sont une rétroaction instantanée au client pour ses propres mouvements et si les mouvements du client sont illégaux, le serveur prend les mesures appropriées (corrige avec force le client).

Qu'en est-il des pertes de paquets et similaires? Le jeu est déterministe, mais je pense que toute divergence dans la séquence des actions exécutées chez un client pourrait conduire à des états du monde incohérents. Comment puis-je me prémunir contre ce genre de problème?

La perte de paquets et le décalage extrême sont un problème difficile à résoudre . Différentes solutions (aucune d'entre elles n'est parfaite) sont employées pour résoudre le problème en fonction du type de jeu (par exemple, l'estimé). Je suppose que votre jeu n'a pas à synchroniser la physique.

L'une des premières choses que vous devriez faire est d'horodater tous vos mouvements. Votre système devrait alors en tenir compte et jouer les mouvements en conséquence (peut-être que le serveur a cette responsabilité et ensuite refuser les mouvements illégaux). Lors de la mise en œuvre de ce système, supposez une latence de 0 (testez localement).

0 latence n'est bien sûr pas une bonne hypothèse, même sur les réseaux locaux, alors maintenant que tous les mouvements respectent le temps, à quelle heure faites-vous confiance? C'est là que la question de la synchronisation de l'heure entre en jeu. De nombreux articles / articles en ligne vous guideront pour résoudre ce problème.

Et si j'ajoute trop d'actions à la fois, cela ne causera-t-il pas des problèmes de connexion? Y a-t-il un moyen d'atténuer cela?

D'abord les trucs évidents. N'envoyez jamais de chaînes ou d'objets sérialisés. N'envoyez pas de messages aussi rapidement que le jeu se met à jour. Envoyez-les en morceaux (par exemple 10 fois par seconde). Il y aura une erreur (en particulier une erreur d'arrondi) dont le serveur / client devra corriger les erreurs de temps en temps (donc toutes les secondes, le serveur envoie tout [tout = toutes les données qui sont importantes pour garder les choses dans votre jeu dans le bon état] auquel le client accroche et / ou prend en compte pour corriger son état).EDIT: Un de mes collègues a mentionné que si vous utilisez UDP [ce que vous devriez, si vous avez beaucoup de données], il n'y a pas de commande réseau. L'envoi de plus de 10 paquets par seconde augmente les changements des paquets arrivant en panne. Je vais voir si je peux citer cette affirmation. Quant aux paquets hors service eux-mêmes, vous pouvez les éliminer ou les ajouter à la file d'attente de messages / déplacements de votre système, qui est conçue pour gérer les modifications du passé afin de corriger l'état actuel.

Vous devriez avoir un système de messagerie qui repose sur quelque chose qui prend très peu de place. Si vous devez envoyer quelque chose qui ne peut avoir que deux valeurs, n'envoyez qu'un seul bit. Si vous savez que vous ne pouvez avoir que 256 coups, envoyez un caractère non signé. Il existe différentes astuces pour bidouiller les messages aussi étroitement que possible.

Votre système de messagerie utilisera très probablement de nombreuses énumérations pour vous permettre d'extraire facilement les mouvements d'un message entrant (je suggère de jeter un coup d'œil à RakNet et aux exemples qu'il contient si vous ne comprenez pas ce que je veux dire ici).

Je suis sûr que vous savez déjà que vous ne pouvez pas résoudre les problèmes qui surviennent avec une latence élevée et la perte de paquets, mais vous pouvez rendre ses effets moins prononcés. Bonne chance!

EDIT: le commentaire de Patrick Hughes ci-dessus avec le lien rend cette réponse incomplète et spécifique à la question de OP au mieux. Je suggère fortement de lire les sujets liés à la place

Samaursa
la source
Merci beaucoup pour la réponse détaillée, qui couvre à peu près toutes mes préoccupations. Juste une question concernant la partie où vous dites so every second, the server sends everything ... which the client then snaps to. Puis-je simplement envoyer une grande quantité d'informations comme celle-ci à la fois? Qu'est-ce qu'une taille maximale raisonnable?
David Gouveia
Un maximum raisonnable sera un nombre qui n'introduit pas de retard:) ... Je sais que vous ne cherchiez pas cette réponse mais je ne veux pas mettre un chiffre parce que je ne connais pas votre jeu.
Samaursa
De plus, il n'y a pas de règle stricte selon laquelle vous devez envoyer un gros morceau, je viens de le donner comme exemple.
Samaursa
@Samaursa - Je ne serais pas d'accord avec le commentaire "Si vous utilisez UDP [ce que vous devriez, si vous avez beaucoup de données]". Puisqu'ils sont nouveaux dans la programmation réseau, TCP s'occupe de la plupart des choses difficiles pour vous, au prix d'être beaucoup plus lentes. Dans leur cas, j'utiliserais TCP jusqu'à ce qu'il devienne un goulot d'étranglement. À ce stade, ils comprendraient mieux comment leur application utilise le réseau, ce qui faciliterait la mise en œuvre des éléments UDP.
Chris
@Chris: Je ne serai pas d'accord :) ... la décision entre utiliser TCP et UDP est comme (imo) la décision entre choisir Python et C ++ pour le développement. En théorie, cela peut sembler correct, mais en pratique, il sera difficile de simplement basculer (encore une fois, c'est imo).
Samaursa