Comment gérer le netcode?


10

Je suis intéressé à évaluer les différentes façons dont le netcode peut "se connecter" à un moteur de jeu. Je suis en train de concevoir un jeu multijoueur maintenant, et jusqu'à présent, j'ai déterminé que je dois (au moins) avoir un thread séparé pour gérer les sockets réseau, distinct du reste du moteur qui gère la boucle graphique et les scripts.

J'avais un moyen potentiel de créer un jeu en réseau entièrement monothread, qui consiste à le faire vérifier le réseau après le rendu de chaque image, en utilisant des sockets non bloquants. Cependant, ce n'est clairement pas optimal car le temps nécessaire pour rendre une trame est ajouté au décalage du réseau: les messages arrivant sur le réseau doivent attendre la fin du rendu de la trame actuelle (et de la logique du jeu). Mais, au moins de cette façon, le gameplay resterait plus ou moins fluide.

Le fait d'avoir un thread séparé pour la mise en réseau permet au jeu d'être complètement réactif au réseau, par exemple, il peut renvoyer un paquet ACK instantanément à la réception d'une mise à jour d'état du serveur. Mais je suis un peu confus quant à la meilleure façon de communiquer entre le code du jeu et le code réseau. Le thread de mise en réseau poussera le paquet reçu dans une file d'attente, et le thread de jeu lira à partir de la file d'attente au moment approprié au cours de sa boucle, nous ne nous sommes donc pas débarrassés de ce délai d'une trame.

En outre, il semble que je souhaiterais que le thread qui gère l'envoi de paquets soit distinct de celui qui vérifie les paquets qui descendent, car il ne pourrait pas en envoyer un lorsqu'il est au milieu de vérifier s'il y a des messages entrants. Je pense à la fonctionnalité de selectou similaire.

Je suppose que ma question est, quelle est la meilleure façon de concevoir le jeu pour la meilleure réactivité du réseau? De toute évidence, le client doit envoyer l'entrée utilisateur dès que possible au serveur, afin que je puisse faire venir le code net-send immédiatement après la boucle de traitement des événements, à la fois à l'intérieur de la boucle de jeu. Est-ce que cela a un sens?

Réponses:


13

Ignorez la réactivité. Sur un LAN, le ping est insignifiant. Sur Internet, un aller-retour de 60 à 100 ms est une bénédiction. Priez les dieux du décalage pour que vous n'obteniez pas de pointes de> 3K. Votre logiciel devrait fonctionner à un nombre très faible de mises à jour / s pour que cela soit un problème. Si vous tirez pendant 25 mises à jour / s, vous disposez d'un délai maximum de 40 ms entre le moment où vous recevez un paquet et agissez en conséquence. Et c'est le cas à filetage unique ...

Concevez votre système pour la flexibilité et l'exactitude. Voici mon idée sur la façon de connecter un sous-système réseau au code du jeu: Messagerie. La solution à de nombreux problèmes peut être la «messagerie». Je pense que la messagerie a guéri le cancer chez les rats de laboratoire une fois. La messagerie me fait économiser 200 $ ou plus sur mon assurance auto. Mais sérieusement, la messagerie est probablement le meilleur moyen d'attacher n'importe quel sous-système au code du jeu tout en conservant deux sous-systèmes indépendants.

Utilisez la messagerie pour toute communication entre le sous-système réseau et le moteur de jeu, et d'ailleurs entre deux sous-systèmes. La messagerie inter-sous-système peut être aussi simple qu'un blob de données transmises par un pointeur en utilisant std :: list.

Ayez simplement une file d'attente de messages sortants et une référence au moteur de jeu dans le sous-système réseau. Le jeu peut vider les messages qu'il souhaite envoyer dans la file d'attente sortante et les envoyer automatiquement, ou peut-être lorsqu'une fonction comme "flushMessages ()" est appelée. Si le moteur de jeu avait une grande file d'attente de messages partagée, tous les sous-systèmes qui devaient envoyer des messages (logique, IA, physique, réseau, etc.) peuvent tous y envoyer des messages où la boucle de jeu principale peut alors lire tous les messages. puis agissez en conséquence.

Je dirais que l'exécution des sockets sur un autre thread est très bien, mais pas obligatoire. Le seul problème avec cette conception est qu'elle est généralement asynchrone (vous ne savez pas exactement quand les paquets sont envoyés) et qu'il peut être difficile de déboguer et de faire apparaître / disparaître de manière aléatoire des problèmes liés au timing. Pourtant, si cela est fait correctement, aucun de ces éléments ne devrait être un problème.

À partir d'un niveau supérieur, je dirais que la mise en réseau est distincte du moteur de jeu lui-même. Le moteur de jeu ne se soucie pas des sockets ou des tampons, il se soucie des événements. Les événements sont des choses comme "Le joueur X a tiré" "Une explosion au jeu T s'est produite". Celles-ci peuvent être interprétées directement par le moteur de jeu. D'où ils sont générés (un script, l'action d'un client, un joueur IA, etc.) n'a pas d'importance.

Si vous traitez votre sous-système réseau comme un moyen d'envoyer / recevoir des événements, vous obtenez un certain nombre d'avantages par rapport à un simple appel de recv () sur un socket.

Vous pouvez optimiser la bande passante, par exemple, en prenant 50 petits messages (de 1 à 32 octets) et demander au sous-système réseau de les empaqueter en un seul gros paquet et de les envoyer. Peut-être qu'il pourrait les compresser avant l'envoi si c'était un gros problème. À l'autre extrémité, le code pourrait décompresser / décompresser le gros paquet en 50 événements discrets à nouveau pour que le moteur de jeu puisse le lire. Tout cela peut se produire de manière transparente.

D'autres choses intéressantes incluent le mode de jeu solo qui réutilise votre code réseau en ayant un client pur + un serveur pur fonctionnant sur la même machine communiquant via la messagerie dans l'espace mémoire partagé. Ensuite, si votre jeu solo fonctionne correctement, un client distant (c'est-à-dire un vrai multijoueur) fonctionnerait également. De plus, cela vous oblige à considérer à l'avance les données dont le client a besoin, car votre jeu solo aurait l'air correct ou totalement faux. Mélanger et assortir, exécuter un serveur ET être un client dans un jeu multijoueur - tout fonctionne aussi facilement.


Vous avez mentionné l'utilisation d'une simple liste std :: list ou d'une autre pour transmettre des messages. Cela peut être un sujet pour StackOverflow, mais est-il vrai que les threads partagent tous le même espace d'adressage, et tant que j'empêche plusieurs threads de visser avec la mémoire appartenant à ma file d'attente simultanément, je devrais aller bien? Je peux simplement allouer des données pour la file d'attente sur le tas comme d'habitude, et utiliser simplement des mutexes dedans?
Steven Lu

Oui c'est correct. Un mutex protégeant tous les appels à std :: list.
PatrickB

Merci d'avoir répondu! Jusqu'à présent, j'ai fait beaucoup de progrès avec mes routines de threading. C'est tellement génial d'avoir mon propre moteur de jeu!
Steven Lu

4
Ce sentiment disparaîtra. Les gros cuivres que vous gagnez restent avec vous.
ChrisE

@StevenLu Un peu [extrêmement] tard, mais je tiens à souligner qu'il peut être extrêmement difficile d' empêcher les filetages de visser avec la mémoire simultanément , selon la façon dont vous essayez de le faire et l'efficacité que vous souhaitez être. Si vous faisiez cela aujourd'hui, je vous indiquerais l'une des nombreuses excellentes implémentations de files d'attente simultanées open source, de sorte que vous n'avez pas besoin de réinventer une roue compliquée.
Fund Monica's Lawsuit

4

Je dois (au moins) avoir un thread séparé pour gérer les sockets réseau

Non, non.

J'avais un moyen potentiel de créer un jeu en réseau entièrement monothread, qui consiste à le faire vérifier le réseau après le rendu de chaque image, en utilisant des sockets non bloquants. Cependant, ce n'est clairement pas optimal car le temps qu'il faut pour rendre un cadre est ajouté au décalage du réseau:

Cela n'a pas forcément d'importance. Quand votre logique est-elle mise à jour? Il est inutile de retirer des données du réseau si vous ne pouvez pas encore en faire quoi que ce soit. De même, il est inutile de répondre si vous n'avez encore rien à dire.

par exemple, il peut renvoyer un paquet ACK instantanément à la réception d'une mise à jour d'état du serveur.

Si votre jeu est si rapide qu'attendre que la prochaine image soit rendue est un retard important, alors il va envoyer suffisamment de données que vous n'avez pas besoin d'envoyer de paquets ACK séparés - incluez simplement les valeurs ACK dans vos données normales charges utiles, si vous en avez besoin.

Pour la plupart des jeux en réseau, il est parfaitement possible d'avoir une boucle de jeu comme celle-ci:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Vous pouvez dissocier les mises à jour du rendu, ce qui est fortement recommandé, mais tout le reste peut à peu près rester aussi simple, sauf si vous avez un besoin spécifique. Quel genre de jeu faites-vous exactement?


7
Le découplage de la mise à jour du rendu n'est pas seulement fortement recommandé, il est nécessaire si vous voulez un moteur non merdique.
AttackingHobo

Cependant, la plupart des gens ne font pas de moteurs, et probablement la majorité des jeux ne découplent toujours pas les deux. Le règlement du passage d'une valeur de temps écoulé à la fonction de mise à jour fonctionne de manière acceptable dans la plupart des cas.
Kylotan

2

Cependant, ce n'est clairement pas optimal car le temps nécessaire pour rendre une trame est ajouté au décalage du réseau: les messages arrivant sur le réseau doivent attendre la fin du rendu de la trame actuelle (et de la logique du jeu).

Ce n'est pas vrai du tout. Le message passe sur le réseau pendant que le récepteur restitue la trame actuelle. Le retard du réseau est limité à un nombre entier de trames côté client; oui, mais si le client a si peu de FPS que c'est un gros problème, alors il a de plus gros problèmes.


0

Les communications réseau doivent être groupées. Vous devez vous efforcer d'obtenir un seul paquet envoyé à chaque tick de jeu (ce qui est souvent le cas lors du rendu d'une trame, mais devrait vraiment être indépendant).

Vos entités de jeu parlent avec le sous-système réseau (NSS). Le NSS regroupe les messages, les ACK, etc. et envoie quelques (espérons-le un) paquets UDP de taille optimale (~ 1500 octets en général). Le NSS émule les paquets, les canaux, les priorités, les renvois, etc. tout en n'envoyant que des paquets UDP uniques.

Lisez le gaffer sur le tutoriel des jeux ou utilisez simplement ENet qui implémente de nombreuses idées de Glenn Fiedler.

Ou vous pouvez simplement utiliser TCP si votre jeu n'a pas besoin de réactions de contraction. Ensuite, tous les problèmes de traitement par lots, renvoyés et ACK disparaissent. Cependant, vous souhaitez toujours qu'un NSS gère la bande passante et les canaux.


0

N'ignorez pas complètement la réactivité. Il y a peu à gagner à ajouter une autre latence de 40 ms à des paquets déjà retardés. Si vous ajoutez quelques images (à 60 images par seconde), vous retardez le traitement d'une mise à jour de position d'un autre couple d'images. Il est préférable d'accepter les paquets rapidement et de les traiter rapidement afin d'améliorer la précision de la simulation.

J'ai eu beaucoup de succès à optimiser la bande passante en réfléchissant aux informations d'état minimum nécessaires pour représenter ce qui est visible à l'écran. Ensuite, en regardant chaque bit de données et en choisissant un modèle pour cela. Les informations de position peuvent être exprimées sous forme de valeurs delta dans le temps. Vous pouvez soit utiliser vos propres modèles statistiques pour cela et passer des années à les déboguer, soit utiliser une bibliothèque pour vous aider. Je préfère utiliser le modèle à virgule flottante de cette bibliothèque DataBlock_Predict_Float Cela facilite vraiment l'optimisation de la bande passante utilisée pour le graphique de la scène du jeu.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.