Comment mettre en réseau ce système d'entité?

33

J'ai conçu un système d'entité pour un FPS. Cela fonctionne fondamentalement comme ceci:

Nous avons un "monde" -object, appelé GameWorld. Cela contient un tableau de GameObject, ainsi qu'un tableau de ComponentManager.

GameObject contient un tableau de composants. Il fournit également un mécanisme d'événement qui est vraiment simple. Les composants eux-mêmes peuvent envoyer un événement à l'entité, qui est diffusé à tous les composants.

Le composant est fondamentalement quelque chose qui donne à un GameObject certaines propriétés, et comme GameObject n’est en fait qu’un conteneur, tout ce qui a trait à un objet de jeu a lieu dans les composants. Les exemples incluent ViewComponent, PhysicsComponent et LogicComponent. Si la communication entre eux est nécessaire, vous pouvez utiliser des événements.

ComponentManager juste une interface comme Component, et pour chaque classe de composant, il devrait généralement y avoir une classe ComponentManager. Ces gestionnaires de composants sont responsables de la création des composants et de leur initialisation avec des propriétés lues à partir d'un fichier XML.

ComponentManager prend également en charge les mises à jour massives de composants, comme le composant PhysicsComponent où je vais utiliser une bibliothèque externe (qui fait tout dans le monde à la fois).

Pour la configurabilité, j'utiliserai une fabrique pour les entités qui liront soit un fichier XML, soit un script, créer les composants spécifiés dans le fichier (qui y ajoute également une référence dans le bon gestionnaire de composants pour les mises à jour en masse), et puis les injecter dans un objet GameObject.

Vient maintenant mon problème: je vais essayer de l’utiliser pour les jeux multijoueurs. Je ne sais pas comment aborder cela.

Premièrement: quelles entités les clients devraient-ils avoir depuis le début? Je devrais commencer par expliquer comment un moteur mono-joueur déterminerait quelles entités créer.

Dans l'éditeur de niveau, vous pouvez créer des "pinceaux" et des "entités". Les brosses sont conçues pour les murs, les sols et les plafonds, des formes simples. Les entités sont les GameObject dont je vous ai parlé. Lors de la création d'entités dans l'éditeur de niveaux, vous pouvez spécifier des propriétés pour chacun de ses composants. Ces propriétés sont directement transmises à quelque chose comme un constructeur dans le script de l'entité.

Lorsque vous enregistrez le niveau à charger par le moteur, il est décomposé en une liste d'entités et de leurs propriétés associées. Les brosses sont converties en une entité "worldspawn".

Lorsque vous chargez ce niveau, il instancie simplement toutes les entités. Cela semble simple, hein?

Maintenant, pour la mise en réseau des entités, je rencontre de nombreux problèmes. Premièrement, quelles entités devraient exister sur le client dès le début? En supposant que le serveur et le client possèdent le fichier de niveau, le client peut également instancier toutes les entités du niveau, même si elles ne sont là que pour les règles du jeu sur le serveur.

Une autre possibilité est que le client instancie une entité dès que le serveur envoie des informations à ce sujet, ce qui signifie que le client ne disposera que des entités dont il a besoin.

Un autre problème est de savoir comment envoyer les informations. Je pense que le serveur pourrait utiliser la compression-delta, ce qui signifie qu'il envoie uniquement de nouvelles informations lorsque quelque chose change, plutôt que d'envoyer un instantané au client à chaque image. Bien que cela signifie que le serveur doit garder une trace de ce que chaque client sait pour le moment.

Et enfin, comment le réseau devrait-il être injecté dans le moteur? Je pense à un composant, NetworkComponent, qui est injecté dans chaque entité censée être mise en réseau. Mais comment le composant réseau doit-il savoir quelles variables doivent être connectées au réseau et comment y accéder, et enfin comment le composant réseau correspondant sur le client doit savoir comment modifier les variables en réseau?

J'ai beaucoup de difficulté à aborder cela. J'apprécierais vraiment si vous m'aviez aidé sur le chemin. Je suis ouvert aux conseils sur la manière d'améliorer la conception du système de composants, alors n'ayez pas peur de le suggérer.

Charretier
la source

Réponses:

13

C'est une putain de dieu (pardon) bête d'une question avec beaucoup de détails +1 là-bas. Assez pour aider les gens qui tombent dessus.

Je voulais surtout ajouter mes 2 centimes sur le fait de ne pas envoyer de données physiques !! Honnêtement, je ne saurais trop insister là-dessus. Même si vous l'avez optimisé jusqu'à présent, vous pouvez envoyer pratiquement 40 sphères qui rebondissent sous l'effet de la micro-collision et la vitesse de transmission peut atteindre sa vitesse maximale dans une salle tremblante qui ne réduit même pas la cadence. Je fais référence à l'exécution de votre "compression / codage delta", également appelée différenciation des données, dont vous avez parlé. C'est assez semblable à ce que j'allais évoquer.

Dead Reckoning VS Data Differencing: ils sont suffisamment différents et n'occupent pas les mêmes méthodes, ce qui signifie que vous pouvez les implémenter pour augmenter encore l'optimisation! Remarque: je ne les ai pas utilisées ensemble, mais j'ai travaillé avec les deux.

Codage delta ou différenciation des données: le serveur transporte des données sur ce que les clients savent et n’envoie que les différences entre les anciennes données et ce qui doit être modifié. Par exemple, pseudo-> dans un exemple, vous pourriez envoyer les données "315 435 222 3546 33" lorsque les données sont déjà "310 435 210 4000 40" Certaines d'entre elles ne sont que légèrement modifiées et aucune n'est modifiée du tout! Plutôt que cela, vous enverriez (en delta) "5 0 12 -454 -7", ce qui est considérablement plus court.

Les meilleurs exemples peuvent être quelque chose qui change beaucoup plus loin que cela, par exemple, disons que j'ai une liste chaînée contenant 45 objets liés en ce moment. Je veux en tuer 30, alors je le fais, puis je transmets à tout le monde les nouvelles données de paquets, ce qui ralentirait le serveur s'il n'était pas déjà conçu pour ce type de choses, et c'est ce qui s'est passé parce qu'il essayait se corriger par exemple. En codage delta, vous devez simplement mettre (pseudo) "list.kill 30 at 5" et supprimer 30 objets de la liste après la cinquième, puis authentifier les données, mais sur chaque client plutôt que sur le serveur.

Avantages: (Je ne peux penser qu'à un seul pour le moment)

  1. Vitesse: évidemment dans mon dernier exemple que j'ai décrit. Ce serait une différence beaucoup plus grande que l'exemple précédent. En général, je ne peux pas dire honnêtement, par expérience, lequel de ceux-là serait le plus commun, car je travaille beaucoup plus avec des comptes morts

Les inconvénients:

  1. Si vous mettez à jour votre système et souhaitez ajouter des données supplémentaires devant être modifiées via le delta, vous devrez créer de nouvelles fonctions pour modifier ces données! (par exemple, comme précédemment, "list.kill 30 at 5" Oh merde, j'ai besoin d'une méthode d'annulation ajoutée au client! "list.kill undo")

Jugement mort: Simplement dit, voici une analogie. J'écris une carte pour quelqu'un sur la façon de se rendre à un endroit, et je n'inclue que les points où aller, en général, parce que c'est suffisant (arrêtez-vous au bâtiment, tournez à gauche). La carte de quelqu'un d'autre inclut les noms des rues et combien de degrés pour tourner à gauche, est-ce nécessaire? (Non...)

Le point mort est où chaque client a un algorithme qui est constant par client. Les données sont assez simplement modifiées en indiquant quels sont les besoins en données modifiés et comment le faire. Le client modifie les données lui-même. Un exemple est que si j'ai un personnage qui n'est pas mon joueur mais qui est déplacé par une autre personne qui joue avec moi, je ne devrais pas avoir à mettre à jour les données à chaque image car beaucoup de données sont cohérentes!

Disons que mon personnage évolue dans une certaine direction, de nombreux serveurs vont envoyer aux clients des données indiquant (presque par image) où se trouve le joueur et indiquant qu'il est en mouvement (pour des raisons d'animation). C'est tellement des données inutiles! Pourquoi diable ai-je besoin de mettre à jour chaque cadre, où se trouve l'unité et dans quelle direction elle fait face ET qu'elle se déplace? En termes simples: je ne le fais pas. Vous mettez à jour les clients uniquement lorsque la direction change, lorsque le verbe change (isMoving = true?) Et quel est l'objet! Ensuite, chaque client déplacera l'objet en conséquence.

Personnellement c'est une tactique de bon sens. C'est quelque chose que je pensais avoir été habile à inventer il y a longtemps, et qui s'est avéré être utilisé tout le temps.

Réponses

Pour être franc, lisez le message de James et lisez ce que j'ai dit à propos des données. Oui, vous devez absolument utiliser le codage delta, mais pensez également à utiliser le calcul sans jugement.

Personnellement, je voudrais instancier les données sur le client, quand il reçoit des informations à ce sujet du serveur (quelque chose que vous avez suggéré).

Seuls les objets pouvant changer doivent toujours être mentionnés comme étant modifiables, n'est-ce pas? J'aime votre idée d'inclure qu'un objet devrait avoir des données de réseau, à travers votre système de composants et d'entités! C'est intelligent, et devrait bien fonctionner. Mais vous ne devez jamais donner de brosses (ni de données absolument cohérentes) à des méthodes de mise en réseau. Ils n'en ont pas besoin, car c'est quelque chose qui ne peut même pas changer (client à client).

Si c'est quelque chose qui ressemble à une porte, je lui donnerais des données de réseau mais seulement un booléen indiquant si c'est ouvert ou non, alors de quel type d'objet il s'agit. Le client doit savoir comment le modifier. Par exemple, il est ouvert, fermé, chaque client doit le fermer. Vous devez donc modifier les données booléennes, puis animer la porte pour qu'elle soit fermée.

En ce qui concerne la manière dont il doit savoir quelles variables doivent être mises en réseau, je pourrais avoir un composant qui est vraiment un sous-objet et lui donner les composants que vous souhaitez mettre en réseau. Une autre idée est de ne pas seulement avoir AddComponent("whatever")mais aussi AddNetComponent("and what have you")parce que ça sonne plus intelligent personnellement.

Joshua Hedges
la source
C'est une réponse ridiculement longue! Je suis terriblement désolé pour ça. Comme j'avais l'intention de ne fournir qu'une petite quantité de connaissances, puis mes 2 centimes sur certaines choses. Je comprends donc que beaucoup de choses peuvent être un peu inutiles à noter.
Joshua Hedges
3

Était sur le point d'écrire un commentaire mais a décidé que cela pourrait être assez d'information pour une réponse.

Tout d’abord, +1 pour une question aussi bien écrite avec beaucoup de détails pour juger de la réponse.

Pour le chargement des données, je demanderais au client de charger le monde à partir du fichier monde. Si vos entités contiennent des identifiants provenant du fichier de données, je les chargerais également par défaut afin que votre système de gestion de réseau puisse simplement s'y référer pour savoir de quels objets il s'agit. Tous ceux qui chargent les mêmes données initiales doivent avoir le même identifiant pour ces objets.

Deuxièmement, ne créez pas de composant NetworkComponent, car cela ne ferait que répliquer des données dans d’autres composants existants (la physique, l’animation et autres sont des éléments courants à transmettre). Pour utiliser votre propre nom, vous souhaiterez peut-être créer un NetworkComponentManager. Ce serait légèrement différent de l'autre relation entre Composant et Gestionnaire que vous avez, mais cela pourrait être instancié lorsque vous démarrez un jeu en réseau et que tout type de composants ayant un aspect réseau confère au gestionnaire ses données pour qu'il puisse les regrouper. et l'envoyer. C’est là que votre fonctionnalité de sauvegarde / chargement pourrait être utilisée si vous disposiez d’une sorte de mécanisme de sérialisation / désérialisation que vous pourriez également utiliser pour conditionner les données, comme mentionné,

Compte tenu de votre question et du niveau d’information, je ne pense pas avoir besoin d’entrer beaucoup plus dans les détails, mais si quelque chose n’est pas clair, merci de poster un commentaire et je mettrai à jour la réponse pour y remédier.

J'espère que cela t'aides.

James
la source
Vous dites donc que les composants qui devraient être mis en réseau devraient implémenter une sorte d’interface comme celle-ci ?: void SetNetworkedVariable (nom de la chaîne, valeur NetworkedVariable); NetworkedVariable GetNetworkedVariable (nom de chaîne); Où NetworkedVariable est utilisé à des fins d'interpolation et autres tâches réseau. Je ne sais pas comment identifier les composants qui implémentent cela. Je pourrais utiliser l'identification de type à l'exécution, mais cela me semble moche.
Carter