Synchronisez mille vecteurs3 sur le réseau avec un minimum d'octets?

Je fais un jeu avec Unity et j'ai des problèmes pour tenter de synchroniser mon réseau. La simulation déterministe est impossible car la logique interne de Unity est flottante. J'essaie de créer une solution qui fonctionne sur un modèle client-serveur faisant autorité, dans lequel la position de chaque soldat est corrigée par le serveur 5 à 10 fois par seconde. Ceci est réalisé en envoyant des datagrammes UDP, et des mesures seront mises en place pour assurer la livraison. Entre les corrections, le client simule le jeu du mieux possible.

Les soldats ne se déplacent pas souvent en ligne droite. Ils s’assemblent et se heurteront à de nombreux obstacles qu’ils doivent surmonter. Les soldats existent en tant qu'individus et ne font pas partie d'un objet d'escouade.

Il y a des choses à faire pour réduire la consommation de bande passante qui ne sont pas au centre de nos préoccupations, car ce sont au mieux des améliorations temporaires. C’est comme envoyer uniquement les positions des soldats en mouvement et/ou observés et compresser les données avant leur transmission.

Le problème est simple. Si j'ai dix soldats qui courent à peu près tout va bien, mais si j'en ai mille, l'utilisation de la bande passante devient déraisonnable. À tout le moins, nous devons envoyer une identité de soldat (ushort) et une position (float, float, float) par soldat. À l'heure actuelle, il s'agit de 2 + 4 + 4 + 4 = 14 octets . Davantage de données peuvent être nécessaires, par exemple la direction dans laquelle vous faites face (les soldats ne regardent jamais de haut en bas).

14 * 10 * 1000 = 140 kb/s ... beaucoup trop!

La question est la suivante:

How can we reduce the number of bytes sent per soldier to an absolute minimum and still have a synchronised result?

SOLUTIONS POSSIBLES:

1. Une de mes idées était que, comme la vitesse maximale d'un soldat ne changerait pas, cela nous permettrait de corriger la position de tout soldat d'une valeur absolue par rapport à la dernière position connue. Mais cela peut aussi souffrir de désynchronisation.

Au lieu d’envoyer un float pour l’axe XYZ de la position, nous envoyons un sbyte. Puisqu'il s'agit de -128 à 127, nous pouvons le lire/écrire pour dire que la nouvelle position correspond à un pourcentage de la vitesse maximale du soldat depuis la dernière position. Si cela fonctionnait, les données envoyées seraient réduites de 14 à 5 octets. Mais même 36% représentent toujours 50 kb/s pour 1 000 soldats.

Potentiellement, cela pourrait être réduit à 4 octets de temps en temps, car les soldats ne montent pas ou ne descendent pas toujours (axe des Y). Mais c’est comme ignorer les soldats qui ne bougent pas, pas le problème.

2 Une autre idée que j’avais était de réduire l’axe XYZ à 12 bits (1,5 octet). Chaque axe a 4 bits, dont 3 sont utilisés pour trouver une valeur de distance. 3 bits nous permettent de compter de 0 à 8. Il y a six valeurs entre la fin du zéro et la vitesse maximale. Nous avons une gamme de valeurs qui peuvent être utilisées car elles sont "assez proches". L'autre bit détermine s'il est positif ou négatif. Le serveur impose ensuite une correction aux soldats avant de leur envoyer leurs positions au client, ce qui les oblige à s'éloigner de la dernière position pour se conformer à l'une des valeurs "suffisamment proches".

De plus, s'il n'y a pas plus d'un millier de soldats en vie à la fois, nous pouvons affecter un identifiant actif aux soldats et réduire ainsi l'ID du réseau de 16 bits à 10, ce qui nous permet de compter jusqu'à 1024.

Au total, cela réduirait les données envoyées par soldat de 14 octets à 22 bits (2,75 octets). Je soupçonne que cela serait encore moins probable que la première idée de maintenir la synchronisation. Le résultat peut simplement être trop nerveux. Si cela fonctionnait, cela représenterait 20% de la bande passante initiale à 28 kb/s.

...

Toutes les idées sont les bienvenues à ce stade. Je me suis concentré sur la création de vecteurs de plus en plus petits, mais il existe peut-être une meilleure façon de le faire, qui fait quelque chose de complètement différent.

4
ajouté édité
Vues: 1
"La simulation déterministe est impossible car la logique interne de Unity est flottante." Facile là-bas, erreurs d'arrondi et indéterminisme ne sont pas la même chose.
ajouté l'auteur whatsisname, source
Je n'ai pas beaucoup travaillé avec le code de jeu - mais avez-vous déjà testé la performance consistant à compresser simplement les données avant de les envoyer sur le réseau?
ajouté l'auteur mkuse, source
Pourquoi voulez-vous que cette synchronisation se produise? Évidemment, si vous avez besoin de transmettre 140 Ko/s, vous devez transmettre 140 Ko/s. Cependant, si nous comprenons quelles propriétés doivent être garanties par la synchronisation, nous pourrons peut-être aider à identifier des parties de ces données qu'il n'est pas nécessaire de mettre à jour aussi souvent, ou peut-être même à nous règles de "cohérence éventuelle".
ajouté l'auteur Cort Ammon, source
À quelle vitesse désynchronisent-ils et quelle est leur sensibilité aux erreurs? Certaines positions de soldat sont-elles plus importantes que d'autres?
ajouté l'auteur Cort Ammon, source
@EsbenSkovPedersen Non, mais si j'ai bien compris, une compression rapide aura un faible effet. (WinZip compressé à 90% de la taille d'origine?) À moins que vous ne puissiez écrire une réponse spéciale dans une réponse permettant de réduire considérablement la taille brute?
ajouté l'auteur MuZERO, source
@CortAmmon Sync est nécessaire pour s'assurer que les positions de soldat sont les mêmes pour tous les clients. Sans corriger pour les positions le client-serveur se désynchronise très rapidement.
ajouté l'auteur MuZERO, source
@CortAmmon Ils commencent à désynchroniser très rapidement; un troupeau commence à se désynchroniser dans les cinq à dix secondes qui suivent, à moins qu'il ne se déplace en ligne droite, la logique de flottement de Unity semble les rendre extrêmement sensibles aux erreurs. Aucun soldat ou sa position n’est plus ou moins important.
ajouté l'auteur MuZERO, source
@whatsisname C'est très vrai. Mais dans ce cas, je ne sais pas du tout où de telles choses se produisent dans la logique interne et l'algorithme de flocage d'Unity. :( Je ne pense pas non plus pouvoir aider.
ajouté l'auteur MuZERO, source

7 Réponses

Les algorithmes de compression standard sont assez efficaces même au réglage le plus rapide. Comme test rapide, j'ai rempli un fichier texte avec 34 000 octets. Compresser avec 7-zip au réglage le plus rapide m’a donné un fichier de 1k. C'est ~ 3% de la taille originale. Certes, mes numéros ont été stockés en ascii, mais cela montre que des valeurs similaires peuvent être réduites à une petite fraction.

Using standard algorithms can probably be outdone by special techniques discussed in other answers but standard compression techniques should be much faster to implement and less error prone to use well-tested algorithms but if the result is "good enough ®" you can choose if you want to use the gained time to go to market faster or make a sweeter game in areas the user can actually feel.

4
ajouté
Il existe des algorithmes de compression, tels que LZ4, spécialement conçus pour être aussi rapides que possible. J'ai utilisé celui-ci des applications critiques dans les délais.
ajouté l'auteur Frank Hileman, source

Il me semble que si vous avez 1000 gars sur l’écran et qu’ils affluent, la position exacte de chacun d’eux sera aussi importante que lorsque vous en aurez 10?

Pourriez-vous regrouper les soldats lorsque le nombre total augmente? Donc, quand j'ai 10, chaque chose en mouvement est un seul soldat. Quand j'en ai 100, ils forment des groupes de dix, avec une seule position et une formation. Quand j'en ai 1000, ils sont en formations de 100 unités?

Cela vous permettra d’agrandir indéfiniment, même si vous perdez le détail

3
ajouté
non, je comprends, c’est juste que, sauf si vous perdez certaines données à mesure que vous redimensionnez, vous pouvez pousser la limite, mais vous aurez toujours une limite
ajouté l'auteur Ewan, source
si vous avez une position d'équipe puis un positionnement déterministe (ish) des membres de l'équipe en fonction du terrain, etc.
ajouté l'auteur Ewan, source
peut-être les faire-ils prendre à des positions prédéfinies sur la carte? couverture, coins etc?
ajouté l'auteur Ewan, source
Cela simplifierait certainement les choses, de fusionner des unités individuelles en escouades, de sorte que vous ne fassiez que transmettre les positions de l'escouade. Mais je ne veux pas avoir des escouades. Je vais modifier la question pour clarifier.
ajouté l'auteur MuZERO, source
Tu as raison. Cela me fait me demander ... Je suppose que l'on pourrait concevoir le système de telle sorte que, si un groupe d'unités est regroupé, il compte temporairement comme une équipe, de sorte que les mouvements et les dégâts ne comptent pas pour l'individu. Les dommages sont appliqués dans une certaine mesure à ce blob de groupe, et ce n'est que lorsqu'ils commencent à se comporter en tant qu'individus que le serveur envoie leurs positions en tant qu'individus. Hm. Supposons que tant que vous gardez le mouvement dans les limites de l’équipe et lors de la dissolution de l’équipe, demandez à toutes les unités de se déplacer rapidement vers leur position déterminée par le serveur pour pouvoir reprendre leur statut d’individu.
ajouté l'auteur MuZERO, source

Bon nombre des idées que vous avez publiées sont bonnes. À un moment donné, vous atteindrez le point de saturation, il y aura donc des compromis à faire. Certains de ces compromis dépendent de la taille de votre réseau et de la vitesse à laquelle vos unités se déplacent. Je vais essayer d'ajouter quelques options supplémentaires que vous pouvez examiner:

  • Compression (assurez-vous qu'il enregistre réellement les octets)
  • Envoyez XYZ, le relèvement et la vitesse pour pouvoir envoyer les mises à jour moins souvent
  • Les mises à jour entrelacées (c'est-à-dire mettre à jour 1/5 des unités avec chaque message)

Vous pouvez probablement intégrer le relèvement et la vitesse dans un octet. Les 4 premiers bits sont la direction (16 directions devraient être suffisantes sur de petits écrans) et les 4 autres bits pour la vitesse (16 gradations à partir de la vitesse maximale) pour représenter les ralentissements dus aux obstacles.

Cela prend les données de 14 à 6, mais vous n'envoyez que des données pour 200 soldats à la fois. Cela signifie que l'appareil reçoit 2 mises à jour par seconde pour chaque soldat, mais qu'il dispose de suffisamment de données pour interpoler la différence avec précision. Cela fait passer le datagramme de vos 5 * 10 * 1 000 (50 000 octets) à 6 * 10 * 200 (12 000) octets. Toujours pas joli, mais meilleur. Ajoutez une compression LZ4 rapide et vous obtiendrez peut-être cette zone idéale de 1 300 octets où les routeurs auront tendance à laisser votre paquet seul. Cela dépend vraiment de la régularité des données.

Vous pouvez enregistrer quelques octets pour l'identifiant si vous envoyez toujours les données dans le même ordre. Vous pouvez utiliser 2 octets pour le premier identifiant du groupe, puis nous aurons un ordre connu pour l'identifiant de troupe par la suite. Cela nous ramène à 3 * 10 * 1000 + 2 (30 002 octets) ou 4 * 10 * 200 + 2 (8 002 octets).

1
ajouté

XOR la ​​valeur de chaque champ de votre structure de données avec la valeur de structure précédente, puis utilisez un codage d'octet variable ou une compression de bits variable pour envoyer les résultats. Je voudrais collecter les données de tous les soldats dans un flux et utiliser un algorithme de compression rapide (après le processus xor ci-dessus) pour le compresser avant de l'envoyer en bloc. Je recommande lz4 comme algorithme.

0
ajouté

Lorsque 1 000 soldats vous approchent, est-il important que celui qui se trouve à 100 mètres à 50 ° de votre position avancée soit placé exactement quand il y en a 30 qui sont à 10 mètres juste devant vous?

Vous ne pouvez envoyer que les positions des soldats qui se trouvent réellement à la portée des capacités du joueur pour attaquer ou être attaqué. Le reste pourrait être généré sur le client et le joueur ne serait probablement pas capable de faire la différence. Lorsque de vrais soldats pénètrent dans le rayon où ils comptent, envoyez leurs vraies positions au client.

Vous pouvez choisir de ne pas envoyer les positions des soldats qui se trouvent derrière le joueur (ou mieux encore, pas dans leur champ de vision), car ils ne seront pas tracés. Puisque le serveur est la vérité sur le terrain, il peut toujours décider si un soldat derrière le joueur a attaqué et frapper le joueur, mais vous n'avez pas besoin d'envoyer la position du soldat jusqu'à ce que le joueur fasse face à ce soldat.

0
ajouté

L'un des objectifs du regroupement est de créer des comportements émergents difficiles à modéliser. Il est donc délicat de les modéliser avec un petit nombre d'octets.

Une approche possible consiste à ancrer le troupeau à un troupeau "virtuel" dont le comportement est plus déterministe. Mettez à jour l'algorithme de flocage de manière à permettre la "cohérence éventuelle" entre le troupeau réel et le troupeau virtuel. En d’autres termes, ajustez l’algorithme de sorte que le vrai troupeau finisse toujours par s’aligner sur le troupeau virtuel à long terme.

Une fois que vous avez cela, vous pouvez alors commencer à chercher à rendre le troupeau virtuel plus prévisible, en utilisant toutes les approches qui ont du sens (peut-être un calcul en entier au lieu de float), et le véritable troupeau tourne autour de lui. Si vous faites cela, 90% de la communication sera de petites erreurs sans importance dans les emplacements. En fin de compte, vous obtiendrez un soldat qui bouge hors des lignes, parce que le véritable troupeau le drague loin du troupeau virtuel. Ensuite, vous pouvez le traiter comme un cas spécial, ce qui est beaucoup plus efficace que de traiter chaque soldat de cette façon.

0
ajouté

Une autre proposition qui essaie d'éviter d'envoyer des données au lieu de les envoyer efficacement ...

Vous constatez à juste titre que vous ne pouvez pas utiliser la réplication exacte du comportement côté client en raison de la qualité dodgy float, mais vous pouvez faire une réplication client inexacte , puis interpoler régulièrement les valeurs exactes à mesure que les mises à jour proviennent du serveur. C’est une façon joliment compliquée de faire les choses, mais cela peut également offrir d’avantages intéressants comme la compensation de latence (en fait, d'après le code du jeu que j'ai examiné, cela semble être ainsi que certains serveurs effectuent la compensation de latence sous Windows. entrées client: supposons qu’elles vont en ligne droite et les repositionnent sans à-coup en fonction de ce qu’elles ont fait dans le passé et dont le serveur n’était pas au courant à ce moment-là).

Fondamentalement, désactivez les agents, puis mettez-les à jour régulièrement (mais de manière à ne pas envoyer tous les vecteurs 1k dans une seule image) ou lorsqu'ils prennent une "décision" côté serveur (c'est-à-dire un comportement non prédictible). Si le comportement de contrôle «de base» est suffisamment simple, alors une seconde environ de «devinette» client (ce qui impliquera l'intégration d'incohérences en virgule flottante) suivie d'une interpolation douce vers l'emplacement actuel peut être pardonnable.

Vous avez suggéré que le "désynchronisation" est perceptible après environ 5 secondes: cette méthode vous permettra de régler/modifier le taux de mise à jour de sorte que les clients ne soient pas obligés de mettre à jour de façon spectaculaire les positions des agents et de ne pas submerger le réseau par mises à jour inutiles. En effet, le débit peut varier en temps réel pour répondre à l’état du jeu: s’il ya trop d’unités à gérer, il peut alors commencer à échouer normalement au lieu de perdre des images/peu importe.

Je ne suis pas un programmeur de jeu, mais cela semble parfaitement réalisable, même si c'est un peu compliqué. Lorsque vous recevez une mise à jour du serveur, vous pouvez effectuer la «réparation» en douceur aussi compliquée que vous le souhaitez, et pour les tests initiaux, vous pouvez simplement la définir comme un «saut» qui supprime cet élément de misère au début et fournit un bon indicateur de si l'idée va fonctionner ou non.

0
ajouté