Crunchez vos adresses URL
|
Rejoignez notre discord
|
Hébergez vos photos
Page 2 sur 3 PremièrePremière 123 DernièreDernière
Affichage des résultats 31 à 60 sur 65
  1. #31
    Merci .

    Etape 3: Gestion du snapshot
    Note: Ce qui suit concernera uniquement l'architecture serveur (le client aura sa part du gâteau à la fin).


    Avoir un snapshot tout beau tout propre ne suffit pas. Pour optimiser l'envoi des paquets sur le réseau, notre objectif est de réaliser un "Snapshot Delta": un snapshot qui ne contient que les changements d'états des entités, depuis le précédent envoi. Un peu comme une sauvegarde différentielle du jeu. L'idée est de ne pas transmettre des informations que les clients connaissent déjà.

    Par exemple, si le joueur se déplace verticalement, seule sa position Y sera modifiée. Quand le serveur souhaite informer les clients de la nouvelle position du joueur, il est inutile de leur envoyer sa position X. Seulement sa nouvelle position Y.


    Or, pour faire un delta (ou une différence) d'un objet, il faut pouvoir récupérer son état antérieur. Avant de créer notre "Snapshot Delta", il faut savoir comment stocker les précédentes créations. Mais un simple champ previousSnapshot dans la classe NetworkServer ne suffit pas.
    Il faut considérer qu'un joueur ne possède pas le même contexte qu'un autre joueur et qu'il n'a pas besoin des mêmes informations que tous les autres participants de la partie. De plus les joueurs n'ont pas besoin d'avoir une vue sur tous les éléments du jeu, au même moment.
    Du coup, il faut pouvoir gérer les snapshots par client ET par gameobjects.


    (Merci quake3!)


    Alors, je revois la méthode d'envoi du snapshot server - SnapshotWorld - aux clients:

    Au départ, je génère le SnapshotWorld et ses SnapshotGameObject du jeu. Ce snapshot doit contenir toutes les informations de la partie à un temps T.

    Ensuite, pour chaque client, je génère un nouveau objet SnapshotDelta
    Spoiler Alert!
    qui est techniquement une copie vide de SnapshotWorld
    . Je pense que vous aurez compris le but de ce snapshot.
    Pour l'instant, nous n'allons pas étudier ce qui sera dans ce SnapshotDelta. Tout ce que je peux dire, c'est qu'il contiendra une liste de SnapshotGameObjectDelta
    Spoiler Alert!
    qui est une version delta de SnapshotGameObject
    . Je traiterai cette partie dans la prochaine étape et nous allons supposer que j'ai produit par magie notre snapshot delta.




    Avant d'envoyer notre chef d'oeuvre au client, il faut l'historier pour les prochaines générations de snapshots delta.
    Pour cela, je vais réutiliser l'objet ClientGameData - où j'enregistre toutes les informations de connexion du joueur, son identifiant et les gameobjects qu'il utilise. A cet objet, je peux lui ajouter un nouveau champ: une liste de snapshots par gameobject.
    Après avoir produit un SnapshotGameObjectDelta, je le duplique pour sauvegarder sa copie dans la liste que je viens de créer. L'original sera inséré dans le conteneur SnapshotDelta.
    Ainsi, je viens d’historier les snapshots par client et par gameobject. Youpi tralala!

    Dans un même temps, je peux copier les instances de SnapshotWorld pour le serveur, dans une autre liste. Cela ne me sert à rien pour le moment... Mais peux-être dans les prochains chapitres :teaser:.

    Nous pouvons peaufiner cette gestion de snapshots en ajoutant un paramètre: si le client a besoin d'un snapshot delta ou complète (= FULL). Ce paramètre est très important, surtout quand le joueur a besoin de réinitialiser son contexte de la partie après une perte de paquet (Packet loss) ou après une perte de connexion. Ou sur la demande du joueur.


    Après avoir historié tout ca, je peux enfin transmettre mon super SnapshotDelta à mon client (après avoir sérialisé et encodé le tout en Base64, bien sûr).
    Il ne reste plus qu'à reproduire tout ce traitement pour tous les autres clients de la partie, et ca sera finis .





    Tout ca c'est bien... Mais je n'ai parlé que de l'architecture serveur. Et du côté client ?

    Nous pouvons appliquer ce même procédé pour le client: Avant d'envoyer le SnapshotClient au serveur, nous faisons un delta avec le précédent envoi et nous historions chaque version du snapshot. C'est facile (le client ne gère qu'un seul snapshot et ne l'envoi qu'au serveur), ca fonctionne très bien et cela peut grandement optimiser la bande passante.

    Sauf dans un cas spécifique.

    Peux-être que je ne l'ai pas précisé plus tôt, mais il est fortement conseillé que le snapshot client ne stocke que des "états" de commandes: Le joueur appuie actuellement sur la touche "déplacement gauche", le joueur est en train de sauter, etc. Il est compliqué d'informer précisément le serveur que le joueur "tire une seule fois à un instant T", surtout quand le lag est présent. Il existe des solutions à ce problème, mais qui ne sont pas simples à mettre en place.
    Spoiler Alert!
    Rendez vous dans les prochains chapitres!

    Si ce genre d'information détaillée est importante pour le serveur et pour le jeu (ce qui n'est pas rare), rien n'empêche le développeur de le gérer. Par contre, il faudra que le snapshot delta puisse prendre en compte ce détail .


    Allez, la prochaine étape, j'optimise tout ce bordel!
    Dernière modification par Louck ; 30/03/2015 à 12h09. Motif: Précision

  2. #32
    Etape 4: Le snapshot delta

    Enfin, nous entrons dans le vif du sujet .
    Prêt pour optimiser comme un gros bourrin ?



    Sur ceux, j'exagère beaucoup. Nous n'allons pas réduire la taille d'un paquet de 80 octets à 4 octets en un clin d'oeil. Mais nous allons faire le nécessaire pour que notre snapshot soit beaucoup moins lourd.


    Je vais utiliser les méthodes suivantes et dans l'ordre:
    • N'envoyer le snapshot que si c'est nécessaire, selon les conditions du joueur et du jeu.
    • Générer un snapshot delta.
    • Ne pas envoyer un snapshot "vide".


    Nous allons surtout travailler du côté de l'architecture serveur. Je vais reprendre les dires de l'étape 3.


    1) N'envoyer le snapshot que si c'est nécessaire, selon les conditions du joueur et du jeu.

    Après avoir généré le SnapshotWorld du serveur, je veux produire un SnapshotDelta pour chacun des clients. La procédure n'est pas monstrueuse. Pour un client donné, je créé l'objet conteneur SnapshotDelta, vide au départ. Puis, pour chaque SnapshotGameObject de mon SnapshotWorld, je génère un SnapshotGameObjectDelta et je l’intègre dans mon conteneur.
    Petit rappel: Chaque SnapshotGameObject correspond au snapshot d'une entité du jeu. Chaque entité a son propre snapshot, pour un moment T.

    Mais avant, je dois vérifier si le client a vraiment besoin d'être informé de l'état du gameobject ou s'il a besoin de ce SnapshotGameObject.
    Pour cela, j'ajoute la fonction abstraite CanBeExecuteByClient() dans l'interface ISnapshotGameObject. Cette fonction contiendra toutes les vérifications nécessaires qui dépendent du Game Design (exemple classique: "Est-ce que l'entité est visible par le joueur ?"). Cette méthode retourne un booléen. Si le résultat est FALSE, j'arrête de traiter ce SnapshotGameObject et je passe au suivant .


    2) Générer un snapshot delta.

    Après les tests, je prépare le terrain pour créer le fameux SnapshotGameObjectDelta.
    Tout d'abord, je dois faire une copie du snapshot SnapshotGameObject. Cette copie est très importante car je veux garder l'état du snapshot original pour l'historisation serveur et pour les prochains traitements. Sa copie subira des modifications.
    Ensuite, je récupère la précédente version du snapshot en fouillant dans l'historique du client.

    Toutes les informations en main, je peux commencer à comparer les deux versions du snapshot - la version antérieure et la copie de la nouvelle version - et produire notre snapshot delta .
    Cependant, si le client ne possède aucune historique pour le gameobject concerné OU si le client a besoin d'une version complète du snapshot, alors je ne fais aucune comparaison et je renomme la copie en SnapshotGameObjectDelta. C'est tout.


    La comparaison entre ces deux snapshots se fera au niveau de leurs champs. Pour comparer le contenu de ces champs, quelque soit la forme du snapshot, je dois faire de la réflexion: via l'instruction snapshot.GetType().GetFields(), je récupère une liste de FieldInfo, chacun contenant les attributs d'un champ de ma classe SnapshotGameObject.
    En utilisant la méthode FieldInfo.GetValue(), je récupère les valeurs des champs de mes deux instances snapshots, avant de les examiner. Si les valeurs d'un champ sont identiques, alors je fixe la valeur NULL pour ce champ, sur le snapshot copié.

    C'est de cette façon que je compare une version d'un snapshot avec une autre, et c'est ainsi que ma copie de SnapshotGameObject se transforme, au fur et à mesure des comparaisons et modifications, en un SnapshotGameObjectDelta.


    La suite? C'est Protobuf qui s'en charge en mettant en avant l'un de ses atouts: Lors de la sérialisation, le plugin ne prend pas en compte les champs sans valeurs pour amortir la taille de l'objet sérialisé.
    Donc, en définissant les champs inutiles de mon SnapshotGameObjectDelta à NULL, Protobuf ne sérialisera que les champs nécessaires, réduisant ainsi le poids de mon snapshot avant son envoi. Le gain en taille peut être très important avec cette technique, mais nous verrons cela à la fin.


    3) Ne pas envoyer un snapshot "vide".

    Ce point n'est pas très compliqué à faire. C'est même très rapide: après avoir fait la comparaison des versions, je vérifie s'il existe au moins un champ qui possède une valeur dans le SnapshotGameObjectDelta. Si ce n'est pas le cas - si tous ses champs sont à NULL - alors il est inutile d'envoyer le snapshot en question. Je l'abandonne et je passe à la suite.

    Pour compléter, si l'objet SnapshotDelta ne contient aucun SnapshotGameObjectDelta à translater, alors je ne le transmet pas au client.
    En gros, s'il n'y a aucun changement dans le contexte du client, alors le serveur ne lui transmettra rien. 0 octets en somme. N'est-ce pas l'objectif ultime de l'optimisation ? .

    Bon, en réalité, il y aura toujours des échanges entre le serveur et le client, même si le jeu ne bouge pas. Au moins pour savoir si l'un ou l'autre est toujours "vivant" ou présent dans la partie (hearbeat ou ping).





    Après avoir produit mon SnapshotGameObjectDelta, je le duplique:
    • L'un sera historisé pour le client.
    • L'autre sera stocké dans le SnapshotDelta.


    Et c'est finis . Du moins, pour ce SnapshotGameObject et sa version delta. Je dois recommencer toute cette procédure pour les autres snapshots/gameobjects contenus dans le SnapshotWorld.
    A la fin, j'aurais produit mon SnapshotDelta qui sera envoyé à mon client.


    C'est l'heure du test!

    Contexte 1 client + 1 serveur.
    Du point de vue du serveur, je me focalise sur la taille du snapshot.

    Voila ce qu'il se passe quand aucun joueur ne bouge dans le jeu:


    Aucun snapshot généré par le client. Aucun par le serveur. Donc rien sur le réseau. Super!


    Quand un joueur se déplace sur l'axe X ou Y:


    Quand un joueur se déplace sur les deux axes:


    Ces captures sont prisent au moment où le joueur appuie sur les touches du clavier. Comme nous pouvons le voir dans la partie "Receive", le joueur n'envoie qu'un ou deux paquets au serveur quand il active une commande. Il n’enverra pas d'autres paquets tant qu'il ne change pas de direction ou tant qu'il ne s'arrête pas. Le joueur peut se déplacer dans une seule direction pendant plusieurs secondes ou minutes, il n'aura envoyé qu'un seul paquet au total.

    Du côté de l'envoi du serveur, c'est un peu plus mitigé. Par rapport aux résultats de l'étape 2, le paquet pèse 3 octets de plus, pour cause de la nouvelle gestion du snapshot. Mais ce paquet est plus lourd que l'ancienne version lorsque le cube se déplace sur les deux axes. Plus précisément, lorsque le serveur envoie un snapshot FULL au client.
    Sinon, quand le cube se déplace sur une seule axe, la taille du snapshot est moins importante. Ce qui est prévu.

    En dehors de ca, il y a toujours ce problème de nombre d'appels, en moyenne 50, qui ne concorde pas avec le tickrate, qui est fixé à 100. Est-ce que mes méthodes prennent trop de temps à s'exécuter ? Je m'occuperai de ca plus tard.


    Enfin, quand un joueur se déplace sur une axe, dans une partie à 4 clients + 1 serveur. La capture est prise après quelques secondes:


    L'optimisation est là. Quand un cube se déplace sur le jeu, le serveur transmet aux clients la position de ce cube et seulement de ce cube. Le serveur ne va pas spammer les joueurs de la position des autres cubes immobiles.


    Pour conclure, je suis bien content du résultat. Il n'y a que très peu de données en jeu, mais malgré ca le bilan est positif . A retenter avec un jeu un peu plus étoffé!

    Je n'ai pas encore testé l'usage de la bande passante avec cette solution. Mais il y a encore plein de petites choses à régler avant de vérifier ca .



    ----

    En espérant que je m'explique bien .

  3. #33
    (Je le dis en passant, j'ai pas tout lu, mais ça a l'air vraiment intéressant, et je garde définitivement ce topic sous le coude pour le jour où j'aurai besoin d'une telle technologie !)

  4. #34
    N'hésitez pas si vous avez des questions ou demandes Ou si vous voyez des erreurs.

    Etape 5: Et après ?

    J'ai cherché d'autres solutions pour optimiser - encore - la taille et l'envoi des snapshots.
    Je suis venu.
    J'ai vu.
    Mais on m'a vaincu .


    Il existe bien des solutions qui peuvent améliorer la transmission des snapshots, mais ils ne fonctionnent pas toujours aussi bien que l'on pense. En voici une petite liste non exhaustive:


    Compresser le snapshot


    L'API RakNet possède déjà sa propre fonctionnalité de compression de paquet. Mais vu que c'est une grosse boite noir dans Unity, difficile d'aller l'analyser et le configurer à notre sauce.
    Tout ce que je sais, c'est qu'elle compresse la donnée utile du paquet et qu'elle est de plus en plus performante au fur et à mesure des échanges sur le réseau (elle "évolue" en cours de partie).
    Le problème est que pour bien compresser les données, il faut beaucoup de ... données. La compression n'apporte rien de bon avec des petits snapshots.

    J'ai quand même fait le test de compresser mes snapshots, avant leurs envois, avec la bibliothèque intégrée GZipStream et la bibliothèque externe LZ4: avec des snapshots qui font moins de 100 octets, la compression me retourne un résultat qui pèse... plus lourd. C'est qui est l'inverse de ce que je cherche à obtenir.

    L'autre inconvénient de vouloir compresser ces données, c'est que ce n'est pas toujours rapide. La compression peut avoir un impact sur le temps de production des snapshots, surtout quand elle doit être générée en moins de 10ms (tickrate 100). Plus il y aura de joueurs, plus il y aura de snapshots à traiter et à réduire.
    Avec mes contextes de test (1 client + 1 serveur et 4 clients + 1 serveur) et mon jeu de données, je n'ai pas eu de problèmes. Mais dans un vrai environnement de jeu, ce défaut peut se faire sentir.

    Toutefois, ces API peuvent toujours nous servir pour diminuer la taille d'un très gros paquet, tant que ce n'est pas fréquent. Par exemple, c'est utile pour l'envoi d'images, de fichiers, ou de contenu généré procéduralement (ca se dit ?)


    Revoir la sérialisation et l'encodage du paquet
    Je ne vais pas cracher sur Proto-buff. Il fait un très bon travail concernant la sérialisation de notre objet snapshot. Néanmoins, je peux critiquer cet objet et son encodage.

    Pour le moment, je sérialise un objet qui contient une liste de snapshots. Le fait d'utiliser une liste et de nombreux objets ont un coût sur le résultat de la sérialisation. De plus, l'encodage en Base64 peut doubler la taille de mon snapshot. Nous avons vu cela dans les précédentes étapes.
    En revoyant ces éléments, il est possible de minimiser notre paquet final.

    Mais est-ce que cela vaut le coup ? Probablement. Mais ce n'est pas simple et cela peut nécessiter l'utilisation "d'astuces techniques" qui peuvent rendre notre code moins lisible voir moins maintenable (et peut nous faire faire passer pour un guru).

    Il y a sûrement une alternative à utiliser des listes ou l'encodage Base64. Mais si j'utilise ces éléments, c'est pour faciliter ma vie de développeur. Est-ce que ca vaut le coup de redévelopper une fonctionnalité dont certaines personnes - sûrement plus compétentes - ont déjà fait ?

    Et même si j'arrive à le faire, je perdrai un temps fou pour gagner quelques octets sur le paquet.


    Revoir les données du jeu
    Nous revenons toujours à la question des données du jeu. Quoi qu'on en dise, ces données ont un réel impact sur la taille du snapshot et sur sa fréquence.
    J'ai déjà intégré des méthodes abstraites de mises à jour et de tests pour avoir un contrôle sur les snapshots. Le vrai défi est de définir les bonnes informations à mettre dans ces snapshots, à un moment T. C'est principalement un travail technique même s'il faut prendre en compte le design du jeu.

    Sinon, il existe de nombreuses techniques pour pouvoir réduire le nombre de données à soumettre sur le réseau: Dead Reckoning, Space partitioning, Extrapolation/Interpolation, Time Dilation, etc... Mais chacune de ces méthodes ne fonctionnent pas pour tous les types de jeux (un jeu de voiture n'a pas les mêmes besoins réseau qu'un jeu de tir) et ont leurs propres avantages et inconvénients. Je reviendrai là dessus dans un autre chapitre.


    Il existe une autre solution qui peut améliorer grandement les performances de mon architecture réseau:

    Ne plus utiliser les RPC



    L'idée est de ne plus utiliser les RPC. Même si j'ai un meilleur contrôle sur les données, les RPC prennent tout de même de la place dans mon paquet réseau et ont quelques restrictions qui peuvent nous gêner plus tard. Dont son côté "Reliable" (ou le "j'attend la réception du paquet coûte que coûte, au pire de retarder les autres échanges").

    Mieux. Nous pouvons programmer notre propre fonctionnalité réseau en .Net et ne plus utiliser Raknet ou ce qu'offre Unity.
    Cependant, cela impliquerai de réinventer la roue et je n'ai pas forcement le temps de tout recoder .


    Il existe bien une alternative aux RPC... Le State Synchronization, le "truc" que j'ai boudé tout au début car il n'était pas assez performant.
    Aujourd'hui, avec les mises à jours et un peu plus d'expériences, je peux essayer d'adapter mon architecture multijoueurs avec le State Synchronization.


    Tentons le coup!


    Utiliser le State Synchronization

    Pour résumer, le SS permet de synchroniser un objet sur le réseau. Pour l'utiliser il faut passer par le composant NetworkView. Un NetworkView (ou NV pour la suite) a deux paramètres: le propriétaire et l'observé.
    L'observé correspond au composant du jeu que nous voulons synchroniser. Par exemple, nous pouvons synchroniser le composant Transform du gameobject (dont sa position + sa taille + sa rotation). Le propriétaire correspond au joueur qui est.... propriétaire de l'objet. Il se charge alors de soumettre les mises à jours de l'état de l'objet aux autres joueurs.
    Il existe un autre paramètre mais dépendant du contexte du jeu: le SendRate. En gros, c'est notre tickrate .

    L'idée est de réadapter le SS à mon architecture, en utilisant les NV comme des canaux de communications. Pour chaque joueur, il nous faut deux canaux (Client => Serveur et Serveur => Client).
    Donc, je créé un gameobject "Communication" qui regroupe deux gameobjects fils: "ClientToServer" et "ServerToClient". Chacun de ces deux gameobjects possèdent le composant NV, qui observera mon nouveau script: SnapshotCommunication.

    Le rôle de ce dernier script est simple: lors de la "mise à jour", le script récupère le dernier snapshot généré par le client/serveur et le transmet sur le réseau via stream.Serialize().
    Spoiler Alert!
    Précision: Cette dernière méthode ne peut pas envoyer mon snapshot sérialisé - type String - sur le réseau, mais les caractères. J'appel une première fois cette méthode pour avertir les membres du réseau du nombre de caractères à récupérer (stream.Serialize(tailleSnapshot)), avant de transférer les caractères de mon snapshot.


    Il ne me reste plus qu'à initier l'objet "Communication" pour le client et le serveur. Ca va se passer au moment où le client se connecte:
    1. Via RPC (oui, ca sert toujours), le serveur envoi au client son "identifiant" (via la méthode Network.AllocateViewID()). Cet identifiant est très important pour que le client puisse connaitre le propriétaire du canal "ServerToClient".
    2. Le client initialise l'objet "Communication" avec l'identifiant serveur et l'identifiant généré par le client. Ensuite, il envoi son identifiant au serveur.
    3. Enfin, le serveur initie son propre objet "Communication" pour communiquer avec le client (et seulement lui) en utilisant ces deux identifiants.


    Au départ, j'ai voulu initier cet objet du côté serveur, sans l'identifiant client (que je recevrai plus tard). Mais les appels RPC prennent du temps et les NV sont impatients, même s'ils ne sont pas initialisés: ces derniers tentent d'envoyer des données sur le réseau, coûte que coûte, avant de cracher des erreurs par dizaines dans la console d'Unity .

    Il reste encore une chose à régler, qui est la génération du snapshot côté serveur. A l'origine, je génère le SnapshotWorld avant de produire des snapshots pour chaque client, à chaque tickrate.
    Maintenant que ces deux tâches sont séparées, j'ai du revoir le code du serveur pour pouvoir générer le SnapshotWorld tout un certain temps (plus fréquemment que le tickrate/sendrate), avant d'être manipulé par mon nouveau script SnapshotCommunication.

    Quelques paramétrages.. et ca fonctionne :D.
    Faisons une comparaison de la bande passante entre la version RPC et la version State Synchronization. Je vais utiliser l'application NetBalancer pour avoir un résultat peu plus précis (par rapport à l'interface d'Unity):

    Tickrate fixé à 30.
    Vue serveur.

    Contexte 1 serveur + 1 client
    RPC
    • Download: 1.8ko
    • Upload: 3.3ko


    State Synchronization
    • Download: 1.8ko
    • Upload: 2.5ko


    A première vue, le résultat me semble bon . Voyons voir avec ce deuxième contexte:

    Contexte 1 serveur + 4 clients
    RPC
    • Download: 7.2ko
    • Upload: 13ko


    State Synchronization
    • Download: 7.2ko
    • Upload: 20.4ko


    What.
    The.
    Fuck

    Dans le contexte de 4 clients, la version SS consomme beaucoup plus de bande passante à l'envoi serveur!


    ...


    En faisant une recherche, j'ai conclu que c'étais normal et que j'étais dans l'erreur.

    Le problème vient des NV.
    Le problème est qu'ils ne communiquent pas excluseivement avec la personne qui possède le même NV.
    Non.

    Ils communiquent avec TOUT LE MONDE, même avec le joueur qui ne possède aucun objet ou NV dans son contexte!
    D'ailleurs chose drôle. Si le joueur A ne possède pas le NV du joueur B et C, et si le joueur B communique avec le joueur C, alors le joueur A recevra une erreur comme quoi il ne possède pas l'objet en question (en plus de consommer de la bande passante) .

    J'ai beau cherché, je n'ai rien trouvé pour résoudre ce problème. Les méthodes d'Unity pour limiter la communication entre un ou plusieurs membres de la partie ne concernent que les RPC. Malheur...
    Je n'ai rien trouvé pour contourner ce problème. Et même si j'en trouve une, il reste le problème des cheaters à régler (vu que le SS ne permet pas de faire une architecture serveur autoritaire facilement).


    Bref. J'ai perdu de nombreux soirs à travailler sur ce sujet, avec pour conclusion que ce n'est pas du tout adapté à mon architecture et que ce n'est pas plus performant.

    Amen.





    En prenant un peu de recul, ce chapitre sur l'optimisation est moins dense que prévu. J'ai surtout revisité les snapshots, produit des snapshots delta et fait une historique. J'ai tenté d'autres méthodes mais:
    • Soit le résultat n'est pas assez satisfaisant pour être mis en place, ou produit l'effet inverse.
    • Soit la mise en place de la solution est très longue et/ou trop compliquée, nécessitant tout un chapitre/étape pour tout expliquer... pour un résultat pas exceptionnel.


    Ce qui est certain, pour bien optimiser les échanges réseaux, il faut adapter son architecture à son jeu (ou au genre du jeu).
    Chaque jeu a ses propres règles et ses propres fonctionnalités. Certains jeux ne se déplacent que dans une direction en utilisant la force (utilisation du Dead Reckoning), d'autres fonctionnent au tour par tour (abandon du tickrate), quelques-uns utilisent des terrains de jeux très importants (Space partitioning), etc... Chaque jeu a sa ou ses propres méthodes d'optimisations. Des méthodes que nous ne pouvons pas généraliser, malheureusement.


    Il est possible que je vais revenir sur ces techniques quand je traiterai certains cas de mon projet. Mais pour ce chapitre - dans le cas de l'optimisation globale - je ne connais qu'une méthode qui fonctionne très bien: le Delta compressing ou l'utilisation des snapshots delta.

    Si vous en avez d'autres, je suis tout ouïe .



    A très bientôt.
    Dernière modification par Louck ; 21/05/2015 à 19h39.

  5. #35
    Après avoir passé de nombreux soirs sur le State Synchronisation, j'ai conclu qu'il est inutile d'en faire un troisième chapitre. Du coup, j'ai complété la dernière étape du chapitre 2 avec ce que j'ai réalisé.

    Je vais réfléchir sur ce que je vais faire pour le troisième chapitre, mais je vais avoir besoin de temps.

  6. #36
    Toujours aussi intéressant, tout le même plaisir à lire tes chapitres.

    Bon courage pour la suite et prends tout le temps dont tu auras besoin

  7. #37
    Je regarde depuis un moment ton topic et c'est super intéressant.

    J'utilise aussi protobuf au boulot, il y a pas mal d'optimisations possible avec les extensions / optional etc... pour éviter les trop gros paquets.

    Pour le protocole tu peux regarder "quic" http://en.wikipedia.org/wiki/QUIC , c'est google qui fait son custom protocole UDP. ( UDP + fiabilitée)
    Par contre je sais pas si Unity implémente déja quic.

  8. #38
    Unity Network fonctionne via l'API RakNet, qui est particulier. Je ne pense pas que ca intègre QUIC.

    Par contre, cela a l'air très intéressant ton truc . Peux-être qu'un jour je tenterai d'implémenter ce protocole (mais ce n'est pas ma priorité et ca nécessite beaucoup beaucoup de travail).


    Merci pour les encouragements .

    Je promet un petit jeu multijoueurs pour la fin de l'année .

  9. #39
    C'est bien il en faut des gros tarés qui font ce que tu fais.
    Et sinon tout le monde est au courant que la prochaine version d'Unity devrait contenir une toute nouvelle API réseau ? (deux en fait, mehbon)

  10. #40
    Je l'ai déjà précisé je pense. Mais pour la version de référence de mon sujet, je reste sur Unity 4 . Dès que l'API sera disponible et patché, j'irai en parler dans un nouveau chapitre.

    En attendant, ce que j'évoque est valable pour n'importe quelle version, à l'exception de certains points très techniques et de ce qui touche à la couche transport du réseau (et inférieur). Du moins, dans le fond.

  11. #41
    Salut,

    Je suis tes posts avec beaucoup d'intérêt. Merci de prendre le temps de faire ça, c'est une vrai mine d'or.
    Félicitations

  12. #42
    Merci beaucoup .

    S'il y a des points qui sont mal expliquées ou mal formulées, n'hésitez pas à me le dire.

    Je vais avoir un peu de retard sur mon planning (le féniantisme est fatal) mais j'avance doucement sur mon prototype qui me servira de sujet pour la suite .

  13. #43
    http://unity3d.com/unity/whats-new/unity-5.1

    La nouvelle API d'Unity Network est sortie.

    Actuellement, je travaille toujours sur la version 4.6 d'Unity. J'attend que la nouvelle version soit peaufinée/patchée avant de passer là dessus.
    Il y aura donc un décalage dans les chapitres: Le prochain concernera la mise à jour de mon architecture avec cette nouvelle version de l'API (en plus de quelques tests). Ca sera un petit chapitre avant de passer à ce qui était prévu: gérer la latence dans le jeu (interpolation, prédiction, lag compensation, etc.. ).


    EDIT: Après lecture du document, je dois tout revoir . Ca réutilise beaucoup d'éléments que j'ai déjà conçu.

  14. #44

  15. #45
    Je suis entrain de finir le chapitre. J'ai pas mal galéré avec la nouvelle API d'Unity3D, mais je m'en sors vivant.
    Ca sera bon pour lundi ou mardi prochain.

  16. #46
    Chapitre 3 - UNet

    Depuis plus d'un an, l'équipe derrière Unity3D avait pour projet de renouveler son API réseau pour une version beaucoup plus performant et beaucoup plus simple d'utilisation. Les développeurs voulaient faire une API à leur sauce.
    Il n'était pas trop compliqué d'innover. Avant, il n'y avait que le composant NetworkView qui gérait la synchronisation de l'état d'un gameobject sur le réseau. A l'exception de ca, il fallait tout coder soit même, pour administrer les clients de notre serveur, pour mettre en place l'interpolation des états, intégrer le lobby et le matchmaking, ... Bref, quasiment tout.


    Aujourd'hui (enfin, depuis plus d'une semaine), la version 5.1 d'Unity3D est sortie, avec sa première version de l'API UNet.
    Et soyons franc, il y a du gros


    La nouvelle API est divisée en deux:
    • Une API de "bas niveau": Au niveau de la couche de transport. Actuellement, il n'y a pas beaucoup de documentations sur les couches de bas niveaux d'UNet, ni sur son paquet réseau. Ce que nous savons pour la prochaine version de cette API, c'est qu'il y aura d'avantages de fonctions qui vont nous permettre de réaliser une architecture réseau un peu plus complexe pour des projets plus ambitieux, dont des MMO.
    • Une API de "haut niveau", ou HLAPI: Elle fonctionne sur l'API de bas niveau et nous offre de multiples composants pour mettre en place le mode multijoueurs de notre jeu.



    Etant donné les gros changements, je vais commencer doucement en travaillant sur l'HLAPI. Je m'attaquerai à la couche de transport une autre fois .



    Par quoi commencer ?

    Nous pouvons déjà faire la liste des composants essentiels de cette HLAPI:
    • Le composant NetworkManager: Ce composant se chargera de tout. De la connexion du serveur et de ses clients, du chargement des prefabs de la partie, à la gestion des scènes et des messages. D'ailleurs, il contient les objets NetworkClient et NetworkServer... Cela me rappelle quelque chose. Si ce composant n'offre pas ce que nous souhaitons, il est toujours possible de coder notre propre version du NetworkManager (chose que je vais faire pour mon architecture multijoueurs).
    • La classe abstraite NetworkBehaviours: C'est le cousin de notre classe MonoBehaviours. Ils font la même chose, à l'exception que le premier intègre des fonctionnalités liés à la synchronisation d'états. Maintenant l'essentiel de la sérialisation se passe par l'attribut [SyncVars] sur nos propriétés, même si les méthodes OnSerialize() et OnDeserialize() sont toujours utilisables.
    • Les composants NetworkIdentity et NetworkTransform: L'un permet d'identifier l'objet sur le réseau (tiens donc!), l'autre permet de synchroniser (et d'interpoler) automatiquement le composant Transform du GameObject.


    Il existe bien d'autres composants et classes (NetworkAnimator, NetworkLobbyManager, etc...). Mais l'essentiel est là (et je n'en n'ai pas besoin... pour le moment) .

    Même les RPC ont changés: Nous parlons maintenant de Commands (client => serveur) et de ClientRpc (serveur => client). Dixit le manuel, leurs utilisations sont bien plus gourmandes qu'avant, ce qui est génant si je souhaite reproduire mon architecture sur cette nouvelle API... Mais il existe une nouvelle solution : Les messages réseaux.
    Les messages réseaux sont simplement des messages "brutes" qui sont envoyés à un ou plusieurs destinataires. Le mot "brute" est sûrement un peu vulgaire car il est possible d'envoyer toute sorte de messages, que ca soit une simple chaine de caractères ou le résultat de la sérialisation d'une grosse classe.
    Les classes NetworkClient et NetworkServer possèdent de nombreuses méthodes d'envoi de messages, dont un simple Send() et un plus détaillé SendBytes().


    UNet semble bien fourni en composants et en fonctionnalités. Cependant, est-ce qu'il est bien plus performant que l'ancienne API RakNet ?
    Sérieusement, difficile à dire. Les paquets construisent par UNet semblent un peu plus léger que ceux fabriqués par RakNet. Mais ces paquets sont tous de tailles différentes, codifiés, et il n'existe pas de documentations détaillées sur la structure de ces trames (sauf quelques détails du côté RakNet, mais rien de fou).
    Néanmoins, l'API UNet offre au développeur de multiples méthodes au développeur pour pouvoir gérer la transmission de ses données dans sa partie, dont le QoS ("Quality of Service"). Des options qui ne sont pas présents (ou pas assez développés) dans l'ancienne version d'Unity.

    Tant que nous savons bien l'utiliser, UNet sera bien plus performant que RakNet. En théorie.


    Maintenant, la grande question est de savoir si mon architecture réseau marchera toujours avec cette nouvelle HLAPI.



    Pour cela, je pensais faire un prototype maison pour tester le bousin. Mais étant un gros féniant, j'ai préféré faire mes tests sur une démo déjà existante. J'ai pris la démo "2dshooter" dans le lien suivant : http://forum.unity3d.com/threads/une...rojects.331978 .

    C'est un bête shoot'em'up multijoueurs avec des vaisseaux qui tirent et des powerups. C'est très classique et contient le strict nécessaire pour faire un jeu multijoueurs (sans le système de lobby. On ne peut pas tout avoir !)
    Le tout marche avec un seul NetworkManager (et un GUI de test), avec des NetworkBehaviours, et avec un NetworkTransform et un NetworkIdentity pour chaque entité du réseau. Il ne nous faut rien de plus.


    Sans plus attendre, j'ai tenté de réadapter mon architecture homemade pour cette nouvelle API. Au début, je m'imaginais à tout refaire de A à Z, à réutiliser les nouveaux composants d'Unity pour tout optimiser à fond. Mais au final, j'ai quasiement tout copier/coller.

    A quelques détails:
    • Vu que le système de RPC a entièrement changé et semble bien plus gourmand, j'ai décidé d'utiliser les messages réseaux pour transmettre les snapshots entre les clients et le serveur. J'ai commencé à utiliser l'objet StringMessage (qui étend l'interface MessageBase) pour encapsuler mon snapshot. Mais j'ai trouvé mieux: l'objet NetworkWriter qui permet de fabriquer et d'envoyer un [b]MessageBase[b] à partir de données binaires. Avec cette méthode, j'évite la sérialisation du StringMessage et je minimise la taille de mon paquet.
    • Maintenant que le composant NetworkIdentity se charge d'identifier un gameobject sur le réseau, il ne m'est plus nécessaire de gérer leurs identifications. Pourtant, je dois toujours gérer les données des clients et je dois définir qui est propriétaire de quoi.


    J'en ai aussi profité pour améliorer le système de snapshots dans le but de pouvoir instancier un ISnapshotGameObject propre à une entité. Un cube ou une balle ne gêrent pas les mêmes états ou propriétés.


    Dès à présent, tout est remis au propre, je fais mes premiers tests...
    Et je tombe sur un gros problème.



    Ci-dessus, nous voyons en rouge le débit montant et en vert le débit descendant, du côté serveur. Cela se passe au moment où le serveur transfert ses snapshots à mon client. Ce dernier ne fait rien de particulier.
    Le problème est que le débit descendant est supérieur au débit montant, alors que le serveur ne fait qu'envoyer des données.

    J'en ai aucune idée du pourquoi du comment du mystère du truc de cette courbe. Mon hypothèse est que le client informe le serveur de la réception de son message. Vu que le serveur n'encapsule pas beaucoup de données dans son paquet (gloire à l'optimisation!) il est possible que le paquet d’acquittement du client soit plus lourd, mais ca serait assez étonnant.
    Difficile de conclure là dessus, vu que les paquets sont encryptés par sécurité. Mais c'est le seul hypothèse viable que j'ai en tête.
    Il est possible aussi que ca soit un bug de l'API d'Unity. A confirmer dans les prochaines versions.


    Après quelques recherches, j'ai pu corriger le problème en manipulant un peu les canaux et le QoS. Par défaut, les canaux sont configurés sur le mode Reliable (= la livraison des paquets est assurée, mais pas son ordre d'envoi).
    Pour mon architecture, le mode le plus adapté est le StateUpdate: La délivrance des paquets n'est pas assurée et ne livre que l'état le plus récent.
    Suite à ca, le graphique affiche de meilleurs résultats et mon problème n'est plus présent. Mais ce QoS implique que nous devons gérer le packet loss et son ordre de délivrance. Je traiterai ce sujet très bientôt.


    Après avoir tout paramétré, j'ai décidé de tester et de comparer mon architecture avec le State Synchronization d'Unet.

    Tickrate à 10 (oui, par défaut, le tickrate est à 10 sur UNet)
    Vue serveur

    Contexte 1 serveur + 1 client
    Messages Réseaux (mon architecture)
    - Download: 208 o/s
    - Upload: 988 o/s

    State Synchronization (UNet)
    - Download: 290 o/s
    - Upload: 1.5 ko/s


    Contexte 1 serveur + 4 clients
    Messages Réseaux (mon architecture)
    - Download: 1 ko/s
    - Upload: 4 ko/s

    State Synchronization (UNet)
    - Download: 1.5 ko/s
    - Upload: 6.1 ko/s


    A première vue, mon architecture semble bien plus performant que la technique classique de la nouvelle HLAPI. Mais il ne faut pas oublier que cette dernière utilise par défaut le QoS Reliable, plus gourmand en bande passante, pour s'assurer que notre message arrive bien à destination. Chose que je vais devoir m'en charger avec mon architecture (et qui va sûrement me coûter un peu en perfs).

    Malgré tout, mon architecture affiche toujours son gros avantage: J'ai un meilleur contrôle sur les données du jeu et je peux les filtrer si nécessaire. A ce sujet, je suis tombé sur quelque chose d'étrange avec le mode SS d'Unet: même s'il se passe rien à l'écran (vaisseaux immobiles, joueurs ne touchent à rien), les clients et le serveur s'échangent à plusieurs reprises des trames de tailles variables. Je n'ai pas la moindre idée de ce qu'ils foutent à ce moment là.


    En outre de ces éloges.

    Quelque soit les techniques utilisés, l'API d'Unity3D consomme constamment de la bande passante. Tous les clients transmettent, toutes les demi-secondes, un paquet de 57 octets au serveur, avant que ce dernier en fasse autant avec ses clients. Au final, il y a un upload et un download de 114 octets qui sont utilisés, ni plus ni moins, par seconde. Est-ce un système de ping ou de heartbeat ? Dieu seul le sait et Wireshark ne peut nous sauver.


    En parlant de Wireshark, j'ai pu examiner le paquet envoyé par le serveur au client, avec mon architecture.



    Taille du paquet: 80 octets
    Taille de la partie données: 52 octets
    Taille des données utiles (selon mon système): 28 octets

    Nous retrouvons bien l'en-tête de 28 octets pour les protocole IP et UDP. Il nous reste donc à déterminer ce que représente les 24 octets. Il y a au moins l'encapsulation du message là dedans. Mais ensuite ? Est-ce qu'il y a des données propres à Unity3D ou à l'API ? Une description sur l'encryptage ou la compression de la trame ? Des informations propres au serveur ? Raahh le manuel ne donne pas assez de détails!
    Par contre, en comparant avec mon projet sur la précédente version (qui utilise les RPC), la partie donnée est moins dense. Cela peut s'expliquer par l'absence des contraintes des RPC en utilisant les messages réseaux, mais aussi de l'utilisation du QOS unreliable.

    A l'exception de ca, il faudra attendre la prochaine mise à jour de l'API, qui concerne la couche de transport, pour se faire une meilleure idée de ce que cache ces octets en trop.


    Bref. Qu'est ce que nous pouvons dire à tout ce bazar ?

    Dans l'absolu, même si la nouvelle API apporte de nouveaux outils et de nouvelles méthodes sur la réalisation d'un mode multijoueurs à notre jeu, elle n'apporte pas grand chose à mon architecture. Exception à la transmission des messages, qui est une bonne alternative aux RPC ou Commands.

    Mais face à ce qu'offre UNet, mon architecture - même performante - semble très amateur. Je suis content de ce que j'ai fais jusqu'à maintenant, mais le chemin est encore long avant que ma création soit au poil et puisse offrir tout ce qu'il faut pour faire fonctionner un jeu multijoueurs dans de très bonnes conditions.


    D'ailleurs, ca sera ma finalité:

    Comme je l'ai indiqué il y a un petit moment, j'ai pour projet de réaliser un jeu multijoueurs pour la fin de l'année. Je m'y attaque dès le premier août. Vous aurez un peu plus d'informations à ce sujet dans un autre topic .
    Ce jeu multijoueurs n'utilisera que les composants d'UNet. Quand le jeu sera finis, je pourrais l'utiliser comme un modèle d'exemple pour tester et pour améliorer mon architecture .


    A très bientôt .
    Dernière modification par Louck ; 03/07/2015 à 12h11.

  17. #47
    Cool de la lecture !

    Du coup le développement d'une partie multijoueur (en tout cas la partie qui gère le dialogue Client <-> Serveur) devient beaucoup plus simple à mettre en place avec cette nouvelle API ? Si c'est le cas on aura surement plus de modes multi dans les petits jeux amateurs/indés.

    Bon courage pour ton projet, je suivrai ça

    Ps: Y'a une liste qui a un peu foiré dans ton texte

    A quelques détails:[LIST][*]Vu que le système de RPC a entièrement changé et semble bien plus gourmand, j'ai décidé d'utiliser les messages réseaux pour transmettre les snapshots entre les clients et le serveur. J'ai commencé à utiliser l'objet StringMessage (qui étend l'interface MessageBase) pour encapsuler mon snapshot. Mais j'ai trouvé mieux: l'objet NetworkWriter qui permet de fabriquer et ......

  18. #48
    Corrigé, merci .


    Oui, la nouvelle API rend beaucoup plus simple la réalisation d'un jeu multijoueurs sur Unity3D. Il y a même un document qui explique comment transformer un jeu solo à un jeu multijoueurs (grosso merdo):
    http://docs.unity3d.com/Manual/UNetConverting.html

    Par contre, il faut toujours garder en tête que l'API ne va pas s'occuper de tout. Le développeur devra tout de même concevoir son jeu pour le multijoueurs: gérer la synchronisation des états, paramétrer correctement la partie selon les situations (QoS, attributs du NetworkBehaviour, ...), faire attention aux interpolations et aux problèmes de latences...

    Unity3D n'offre que des outils au final, mais les contraintes d'un jeu multijoueurs sont toujours présentes.
    Mais il est vrai que la nouvelle API rend beaucoup plus simple ce travail, par rapport à avant .

  19. #49
    Et bien, le moins que je puisse te dire Lucskywalker, c'est un énorme merci
    Je me suis récemment mis à éplucher/apprendre l'implémentation du multi dans unity et tes explications me sont/seront fort utiles et m'aideront beaucoup.
    Je ne suis pas programmeur de formation et j'avoue que la couche réseau avait quelque chose de...comment dire...trop "abstrait" pour que je l'assimile correctement.
    C'est vraiment très sympa de prendre du temps pour partager tes connaissances avec les autres canards-développeurs !

  20. #50
    De rien .

    Si tu as des questions, n'hésites pas!

  21. #51
    J'ai commencé à touiller la nouvelle API cette semaine mais je suis un peu agacé.

    À ce que j'ai compris les nouveaux NetworkBehavior et l'utilisation des RPC et state synchronization nécessitent de spawner les éléments côté clients suite à une commande serveur.
    Pour mon appli les clients et serveurs comportaient les mêmes scènes à quelques détails près et des NetworkView faisaient le lien. Je me trompe où ça n'est plus possible? Pour mon besoin, quasiment rien n'est instancié au runtime, le reste est déjà présent dans l'éditeur. J'utilise un système d'argument de commandes pour paramétrer le tout ce qui me permet de ne faire qu'un build unique.
    (je tente de l'affichage d'une scène sur plusieurs écrans, une seule machine fait tourner la physique, les autres ne font qu'afficher)

    Du coup j'ai commencé à jouer avec les messages mais pour notre appli en LAN j'ai besoin de transmettre les données au rythme d'unity lui-même (tickrate de 60 donc ) Je ne sais pas si ça tiendra la route. Du coup tu conseilles carrément d'aller au niveau transport et de changer les QOS? (ou peut-on le faire dans la HAPI avec les messages?

    L'utilisation des writer/reader est un peu obscure. Ils donnent en exemple l'envoi d'une trame binaire
    avec du genre
    writer.Startmessage(msgId);
    msg.serialize(writer);
    writer.EndMessage(msgId);
    client.SendWriter(writer);

    Je ne vois pas trop l'intérêt par rapport à simplement implémenter serialize/deserialize dans mon message et ensuite appeler un Send(msg). D'autant plus qu'il nest pas expliqué dans le manuel comment je récupère le résultat de mon sendWriter

    Autre souci de perf, dans les handlers de reception, on caste le message pour le récupérer, mais je suppose que le constructeur et la deserialization du message sont invoqués par unity. Ce qui me pose un souci car ça m'impose l'instanciation de la structure de reception à chaque fois. Si mon message comporte un array ça va vite finir par l'appel au garbage collector, ce qui est ennuyant.
    J'envisage éventuellement de fournir un pool de mémoire appelable dans Deserialize, mais bonjour le code opaque pour l'utilisateur...

    Du coup je risque de taper dans la LAPI, mais c'est quand même irritant de voir 90% du nouveau contenu non utilisé, et reste la question des command/RPC

    PS : je partais en vacances avec au retour un débat sur notre future architecture, je ne m'attendais pas à voir ce sujet abordé ici-même
    Dernière modification par Uriak ; 06/08/2015 à 14h04.
    Mes dessins sur DeviantArt IG et BlueSky

  22. #52
    Je pense avoir compris .

    Tout d'abord, les NetworkViews et l'attribut RPC ne fonctionnent plus avec la nouvelle API. Ils sont toujours là, mais ca reste des fonctionnalités obsolètes (et je n'ai aucune idée s'ils fonctionnent toujours).


    Pour résumer le fonctionnement standard d'UNet (en State Synchronisation):

    Pour chaque gameobject du jeu, si on veut que leurs états soient synchronisés, il faut:
    - Le composant NetworkIdentity (pour identifier l'élément sur le réseau);
    - Tout composants qui peuvent synchroniser son état, héritant du NetworkBehaviour. Ca peut être un composant perso (avec les attributs Async) ou un composant existant (dont NetworkTransform).

    Ensuite, il faut comprendre qu'il existe maintenant un gameobject "Joueur" qui a un fonctionnement particulier: C'est à travers cet objet que le joueur peut communiquer avec le serveur (via l'attribut Commands, sorte de RPC). Cet objet doit être une prefab, pour la propriété "Player Prefab" du NetworkManager. Je ne l'ai probablement pas expliqué dans le dernier chapitre.

    Quand un joueur se connectera au jeu, le prefab sera automatiquement instancié par le serveur.
    Plus de détails ici:
    http://docs.unity3d.com/Manual/UNetPlayers.html

    Ensuite il y a bien cette histoire de spawn ou d'autorité sur l'instance. Mais pour simplifier la chose: Le serveur est le roi et il fait tout. Et si tu veux instancier un prefab sur le réseau, il faut l'indiquer au NetworkManager.


    Concernant les messages réseaux, tu peux lier un type de message à un handler , via RegisterHandler du NetworkClient et NetworkServer.
    Ensuite en utilisant le NetworkWriter, tu indiques le type de ton message dans la méthode StartMessage(). Et ca sera bon .
    Pour la réception du message dans le handler, je n'ai pas trouvé mieux que de déserialiser avec la classe StringMessage, avant de le convertir en un tableau d'octets (via Convert.FromBase64String()).

    Après, c'est une façon comme une autre de transmettre un message au serveur ou aux clients de la partie. Passer par les méthodes serialize/deserialize du message est une autre façon de faire. Par contre, aucune idée si cette technique est bien plus performante.

    Par contre, rien ne t'empêche d'avoir un tickrate client différent au tickrate serveur, surtout si la connexion cliente ne permet pas un bon upload. De plus, qui dit que le client va effectuer une tâche toutes les 16 millisecondes ?
    Au mieux, informes le serveur des changements d'états du client (s'il marche ou non, dans quelle direction, etc...). En faisant un delta entre la précédente et la nouvelle état, tu sauras quand il faudra transmettre un message au serveur (avec une limitation bien sûr, pour éviter le flood de message).


    Ne t’embêtes pas avec les problèmes du GC ou d'optimisations pour l'instant. Essayes déjà de faire fonctionner ton projet avant de travailler avec la couche de transport de l'API (surtout qu'à part vouloir faire son propre réseau avec ses propres méthodes, HLAPI fournit déjà tout ce qu'il faut).


    Est-ce que je répond à tout ?

  23. #53
    Network et Network view fonctionnent encore même si ça génère des warnings du type deprecated (encore heureux)

    En fait ma question portait plutôt sur le fait de savoir si les gameobjects comportant un networkidentity devaient bien être des prefabs instanciés suite à une commande serveur.

    Dans notre utilisation précédente on avait disons un Gameobject avec un networkview. Côté serveur, un component supplémentaire modifie la transform associée, modification reportée via le NetworkView. Mais l'important est que les gameobject sont présents dans la scène au départ des DEUX côtés. J'ai l'impression que le système des NetworkIdentity ne s'applique en fait qu'à des prefabs, tu me confirmes? Si je veux symplement synchroniser deux transforms je vais devoir les instancier grâce au player.

    Pour le reste J'ai déjà pu tester une alternative avec les messages, dans lesquels j'encode une série de displacements (la combinaison d'un Vector3 et d'un Quaternion.) En surchargeant serialize/deserialize, ça fonctionne mais ce qui m'ennuie c'est qu'au décodage (et un string message aurait le même soucis) il faut instancier les classes de receptions (un string dans ton cas, deux arrays dans le mien), parce que deserialize ne prend en argument que le writer. J'aimerais être capable en fait d'acoir une fonction du type

    Deserialize (reader/stream/bytes, Vector3[] res) ce qui me permettrait de garder un buffer de recption dans lequel j'écrirais sans instanciation à chaque lecture. (encore une fois parce que le GC débarque de ce fait régulièrement et provoque un hiccup)

    Pour revenir à l'encodage des messages ma question serait du genre : si j'envoie explicitement un message via sendWriter, quelle différence avec juste sendMessage? En plus dans ce de dernier cas, je sais quel type réceptionner dans mon handler du genre msg.read<MaClasseDeMessage>(); Si je fais un sendWriter j'ai juste ajouté le short msgType qui indiquel quel handler(s) vont le prendre en charge.

    et merci pour les réponses
    Dernière modification par Uriak ; 06/08/2015 à 14h30.
    Mes dessins sur DeviantArt IG et BlueSky

  24. #54
    En fait ma question portait plutôt sur le fait de savoir si les gameobjects comportant un networkidentity devaient bien être des prefabs instanciés suite à une commande serveur.
    A vrai dire, je n'ai jamais fait quelque chose de ce genre. Mais dixit la doc d'Unity sur le NetworkIdentity:
    The NetworkIdentity is used to synchronize information in the object with the network. Only the server should create instances of objects which have NetworkIdentity as otherwise they will not be properly connected to the system.
    En théorie, les gameobjects liés à une scène seront instanciés au lancement de cette scène. Même s'ils ont le composant NetworkIdentity, ils ne seront pas instanciés par le serveur, et donc ne seront pas synchronisés sur le réseau.

    Mais comme je l'ai dis, je n'ai jamais testé ce cas là. Mais je doute que ca fonctionne autrement.


    Donc oui, le mieux est de pouvoir instancier des prefabs qui possèdent le composant NI. Et de les enregistrer dans le NetworkManager avant de pouvoir les générer.

    Par contre attention, c'est le serveur qui synchronise l'état des objets de la partie (les [ASYNC]). Pas le client, même s'il peut avoir une autorité sur un objet.


    Pour le reste J'ai déjà pu tester une alternative avec les messages, dans lesquels j'encode une série de displacements (la combinaison d'un Vector3 et d'un Quaternion.) En surchargeant serialize/deserialize, ça fonctionne mais ce qui m'ennuie c'est qu'au décodage (et un string message aurait le même soucis) il faut instancier les classes de receptions (un string dans ton cas, deux arrays dans le mien), parce que deserialize ne prend en argument que le writer.
    Là-dessus, je suis assez d'accord, ce n'est pas une fonctionnalité qui est très pratique.
    En effet, je pense que tu peux trouver une solution dans la couche de transport. Mais est-ce que ca vaut le coup pour un simple problème de GC ?


    Pour revenir à l'encodage des messages ma question serait du genre : si j'envoie explicitement un message via sendWriter, quelle différence avec juste sendMessage? En plus dans ce de dernier cas, je sais quel type réceptionner dans mon handler du genre msg.read<MaClasseDeMessage>(); Si je fais un sendWriter j'ai juste ajouté le short msgType qui indiquel quel handler(s) vont le prendre en charge.
    C'est une façon comme une autre d'envoyer des messages. Ces fonctions ont les mêmes retours et peuvent invoquer le même handler, mais possèdent des paramètres et un fonctionnement différents.
    Dans mon cas, au départ je souhaite transmettre la sérialisation d'un Snapshot, et je ne voulais pas m’embêter à créer une classe Message. Au pire, je créé un "MessageSnapshot" qui puisse contenir ma suite d'octets. Mais dans ce cas, autant faire un StringMessage avec un bon encodage. Au final ca fait la même chose.

    Le NetworkWriter est la solution que j'ai prise car cela m'évite de gérer/instancier une classe pour une simple donnée. Mais ca finit de la même façon que les autres méthodes.


    Techniquement, je ne sais pas ce qu'ils se passent dans ces méthodes. Il faudra inspecter les trames pour se faire une idée des données qui sont envoyées.


    PS: Concernant le QoS, tu peux le paramétrer dans le NetworkManager.

  25. #55
    Que veux tu dire par ça t'évite d'instancier une classe pour une simple donnée?

    Typiquement dans mon cas j'ai dans ma classe un MaClasseDeMessage msg instancié au start. Je mets à jour son contenu au fil de l'eau et dans l'update je fais un NetworkServer.SendAll(msg); Je n'instancie rien à chaque fois.
    Ce que tu me dis me rassures je vais pouvoir jouer avec la QoS j'ai un souci (peut être ) concernant l'ordre de de réception.
    Mon problème est moins celui de la bande passante (on bosse en LAN) que du délai. Surtout si onsouhaite afficher la même scène sur plusieurs machines. Jusque là j'utilisais une lib externe mais elle pose trop de soucis et je souhaitais passer à aute chose, d'autant qu'un test d'un serveur vers 10 clients utilisant les networkview m'a un peu refroidi question rendu...


    j'avoue que j'aime bien le C# mais dès fois je regrette de ne pas pouvoir utiliser la pile plus que ça, surtout si c'est pour manipuler de grosses données.
    Mes dessins sur DeviantArt IG et BlueSky

  26. #56
    Que veux tu dire par ça t'évite d'instancier une classe pour une simple donnée?
    Je manipule une simple chaîne de caractère, je n'ai pas besoin de faire une nouvelle classe pour un simple String.
    Sinon, si je devais passer par le système de message classique,je peux utiliser la classe StringMessage pour mon cas. Mais à partir de là je ne sais pas ce qu'il fait, et j'ai peur qu'il fasse un traitement inutile sur ma donnée (comme la sérialiser... de nouveau).

    C'est (aussi) pourquoi j'utilise le NetworkWriter car je sais ce que je transmet sur le réseau.


    Surtout si onsouhaite afficher la même scène sur plusieurs machines.
    Pour la manipulation de grosses données, tu peux passer par un système de compression (lz4) et de tout transmettre au départ au joueur. Ensuite s'il y a des modifications dans la scène, le mieux est de pouvoir informer le joueur de ces changements et de ne pas tout re-transférer.
    N'hésitez pas à jouer avec le QoS, surtout si vous avez beaucoup de données à transférer, plus ou moins importants.

    Sinon le plan B est de faire du Streaming. Mais je ne suis pas sûr que Unity3D soit la meilleure solution pour ca. A voir dans la couche de transport si tu peux faire quelque chose à ce sujet.

  27. #57

  28. #58
    Je fais une courte parenthèse concernant le tickrate, suite à une discussion sur le topic Overwatch.

    La question sur le topic:
    Excusez mon ignorance, mais un tickrate de 20, c'est une bonne ou une mauvaise nouvelle ?
    Ma réponse sur le topic:
    Si on exagère, c'est la pire mauvaise nouvelle que nous pouvons avoir. Mais uniquement si on exagère .

    Le problème du mauvais tickrate peut se révéler dans des jeux "très rapides" comme Quake. Quand le joueur peut se déplacer à une très grande vitesse, faire des bunny hopes, tourner/esquiver quand il veut, il est essentiel que le joueur en face en soit informé au plus vite, et le plus précisément possible.

    Le tickrate est surtout pour rendre le monde plus cohérent, plus précis aux yeux du joueur. Pendant que les données s'échangent sur le réseau, le monde géré par le serveur évolue fluidement.
    Pour rassurer, avec les techniques utilisés aujourd'hui, ne pas avoir un très gros tickrate n'est pas si pénalisant que ca, dans une partie publique. Après, tout dépend du jeu en question (edit: et de comment c'est coder, bien sûr).

    Suite à ca, un canard fait la réflexion suivante:
    BF n’est pas un jeu super rapide comme Quake et pourtant c’était extrêmement gênant. Quant au fait que de nos jours un tickrate bas ne soit pas pénalisant j’aimerais bien un exemple parce que là je vois pas
    Ainsi, et parce que j'ai un peu de temps à perdre, je me suis à coder quelques exemples

    EDIT: Je n'ai jamais joué à BF. J'ai juste entendu qu'ils ont un mauvais netcode, mais cela m'étonnerai que la faute provient du mauvais tickrate étant donné le genre du jeu. Mais si vous avez des exemples, je veux bien les étudier .


    Tout d'abord, lorsque je parle du tickrate, je fais référence à la fréquence d'envoi des snapshots/mises à jours/données sur le réseau par le serveur ou le client.

    Spoiler Alert!
    Il y a bien eu une époque où le tickrate pouvait faire référence au rafraîchissement du jeu - du contexte et de ses entités - par le serveur. Mais c'étais dans une période où les ordinateurs n'étaient pas forcement très puissant pour pouvoir gérer des grosses parties, contrairement à aujourd'hui.





    Première démo

    A gauche: vue serveur
    Au milieu: vue client, tickrate 20
    A droite: vue client, tickrate 100


    (http://s1.webmshare.com/zYPgm.webm, pour voir en plus grand)

    La démo est en deux parties:
    - Du côté serveur, les cubes suient un parcours identique, à vitesse égale. Fréquemment, il transmet au client la position courante du cube à un moment T (T= selon le tickrate fixé. Si c'est 20, ca sera toutes les 0.05 secondes, si c'est 100, ca sera toutes les 0.01 secondes).
    - Du côté client, dès qu'il reçoit une donnée du serveur, il met à jour la position du cube (en le "téléportant").

    Le cube de gauche est une exception à la démo, car il sert à montrer la différence entre la vue serveur et la vue client.


    C'est un exemple très classique: le mouvement de déplacement du cube de milieu (tickrate 20) est saccadé, alors que celui de droite (tickrate 100) est fluide.
    La vitesse est assez lente, du coup on a l'impression que le cube de droite (100) est très fluide. Mais si j'accélère la vitesse, il est possible que le cube saccade légèrement . Dans les deux cas, nous avons vraiment l'impression que le cube se déplace, contrairement à son copain du milieu (tickrate 20) qui "saute" à un certain rythme.

    La position du cube ne change pas tant que le client ne reçoit pas la donnée. C'est un peu ce qu'il se passe au niveau du réseau, et c'est ce que je souhaite montrer en démo (en accéléré ).


    De ce point de vue, je suis d'accord que de jouer à un jeu avec un faible tickrate, ce n'est pas très plaisant...


    Cependant, nous sommes en 2015 et des techniques permettent de masquer cette horreur.


    Seconde démo

    (http://s1.webmshare.com/NMznX.webm)

    Vue client exclusivement, étant donné que la vue serveur n'a pas changée depuis (sauf la latence).

    Ceux deux cubes sont configurés sur deux tickrates différents: l'un est à 100, l'autre est à 10.
    Savez-vous qui est qui ?


    Pour cette seconde démo, j'ai appliqué la technique de l'interpolation: au lieu de mettre à jour automatiquement et brutalement la position du cube, le client le mémorise et fait "déplacer" le cube vers cette nouvelle position.
    A l'exception d'un léger décalage sur la position de départ des cubes (dont mon code est le coupable), ces cubes se déplacent avec fluidité sur le terrain.


    Cependant, faire de l'interpolation ne sauve pas un mauvais tickrate d'un jeu très rapide: si un personnage change de directions des centaines de fois par seconde, le client ne récupérera qu'une fraction de toutes ces modifications (en y applique une "moyenne"). Pour un jeu comme Quake, c'est impardonnable. (EDIT) Dans ce cas, il est important d'avoir un tickrate élevé.
    Si besoin, je peux faire une démo qui reproduit ce cas. Mais il me faudra une meilleure capture vidéo .




    Ce qu'il faut prendre conscience dans une partie multijoueurs avec une architecture client/serveur, c'est que le client n'est qu'un clavier attaché à une fenêtre, qui a une vue (décalée) sur une partie gérée par le serveur. Le "tickrate" ne sert qu'à rafraîchir plus ou moins souvent cette vue, et il existe de nombreuses techniques pour rendre l'expérience du jeu plus ou moins fluide.

    Le "tickrate" n'est pas d'impact sur les règles du jeu ou le gameplay.
    Le "tickrate" n'est pas coupable d'un mauvais test de collision ou d'un tir à travers les murs.
    Par contre, il l'est si un des personnage peut agir et peut se déplacer bien plus rapidement que le taux de rafraîchissement.


    Le tickrate ne sert qu'à ça. C'est pour cela que je dis qu'un tickrate faible n'est pas forcement signe d'un très mauvais netcode. Par contre, vous pouvez vous plaindre sur la compensation de latence (ou lag compensation) .


    PS: Je ne fais pas de démo pour le tickrate côté client=>serveur (ou sendrate, comme certains l'évoquent). C'est un cas totalement différent et bien moins problématique s'il est bien géré.

    PS2: Je n'ai pas eu le temps, mais pour ceux qui veulent je peux leur fournir une version compilée du projet, pour tester chez eux.
    Dernière modification par Louck ; 25/11/2015 à 16h14.

  29. #59

  30. #60
    Tiens je reviens sur ce sujet pour dire qu'au final après avoir fait fonctionner UNET, j'ai été relativement agacé par ses contraintes sur notre modèle de développement. Nous faisons fonctionner une architecture client-serveur avec différentes machines de visualisation qui ne font qu'afficher une scène coordonée par une machine maître.

    Dans ce contexte UNET
    - comporte quantité de réglages non nécessaires à une utilisation en LAN
    - impose des contraintes sur la scène initiale. Les éléments utilisant les NetworkIdentity devant être instanciés à la volée ou activés une fois le localhost établi
    - propose un rafraîchissement décevant pour les network tranform
    - l'appel aux RPC ne s'applique pas à la machine connectée au localHost (ce qui oblige à dupliquer le code à chaque appel)

    Du coup j'ai finalement conclu "fuck this sh*t" et j'ai ré-implémenté tout ce dont j'avais besoin à l'aide de la librairie NetMQ (implémentation c# de ZeroMQ), en particulier j'ai refais les FPS et la synchro de variable à la mode de l'ancien network Unity (CallRPC("toto", RPCMode.ALL, args)
    Je ne recommande pas la démarche en général mais clairement l'outil n'était pas adapté aux besoins de mon cas.
    Mes dessins sur DeviantArt IG et BlueSky

Règles de messages

  • Vous ne pouvez pas créer de nouvelles discussions
  • Vous ne pouvez pas envoyer des réponses
  • Vous ne pouvez pas envoyer des pièces jointes
  • Vous ne pouvez pas modifier vos messages
  •