Crunchez vos adresses URL
|
Rejoignez notre discord
|
Hébergez vos photos
Page 1 sur 3 123 DernièreDernière
Affichage des résultats 1 à 30 sur 65
  1. #1
    Bonsoir!

    Depuis mon précédent jeu sous Java (Punxel Agent), j'ai signé un pacte avec Lucifer et j'ai installé Unity3D sur mon PC.

    Cela fait maintenant plusieurs mois que je m'amuse avec et je suis conquis. Tellement conquis que je souhaite réaliser le jeu de mes rêves: Jouer un pirate de l'espace avec d'autres joueurs en coopération.

    Mais beaucoup le savent: implémenter un mode "multijoueur en ligne" dans un jeu, c'est très difficile. Il faut voir les nombreux jeux indés qui ne comportent pas ce mode de jeu pour comprendre: Samurai Gunn, Towerfall...
    Et même s'il y a la possibilité de jouer avec ses copains à distance, beaucoup de jeux possèdent un netcode pas très performant, ou pas très cohérent (coucou Battlefield 4 ).


    Mais, étant fou et avec du temps de libre à perdre, je vais tenter cet aventure . Et ce topic sera mon devlog.

    Pour résumer, dans ce topic, je vais écrire mes avancés sur la réalisation d'une architecture multijoueur sous Unity3D. Avec quelques conditions:
    • Je n'utilise que ce que m'offre Unity3D pour réaliser mon projet. Pas de uLink, de Photon machin, ou autres API.
    • L'architecture sera client/server avec un serveur autoritaire (pour limiter les possibilités de triche).
    • Une architecture assez modulable pour être réutilisée dans plusieurs projets.
    • Netcode optimisé (du moins, pour le format coop 8 joueurs ).


    Ma finalité est de ne pas réaliser un jeu, mais une sorte d'API pour pouvoir intégrer facillement (et rapidement) un mode multijoueur pour mes futurs jeux.
    De plus, mon but est de ne pas produire une architecture parfaite. Du moins, il faut que ca marche .


    Ma façon de procéder est simple: Je débute tout à zéro en mode amateur. J'ai les connaissances, mais aucune pratique (même si j'ai déjà codé un FTP en Java).
    Je ne vais pas tenter de reproduire ces architectures, mais je vais m'y baser:
    http://fabiensanglard.net/quake3/network.php
    http://www.gamasutra.com/view/featur...8_network_.php
    https://developer.valvesoftware.com/...yer_Networking

    Attention: j'écrirai en tant que développeur, pour les développeurs. Certains termes risquent de ne pas être compréhensibles pour les simples joueurs (Callback? Méthode abstraite?).
    J'essayerai d'expliquer au mieux ma démarche au fil du projet, à chaque étape. Mais si j’explique mal ou si je ne détail pas assez, n'hésitez pas à m'en avertir .
    Je tenterai de faire un "rapport" chaque semaine au minimum. Mais je préviens: ca sera des mises à jours irréguliers (vu mes disponibilités). Je ferais de mon mieux.

    Si vous avez des questions ou des envies, n'hésitez pas .


    FAQ1: Si j'utilise Unity3D pour ce projet, c'est parce que je veux réaliser un jeu sur le long terme (mais pas maintenant).
    FAQ2: Je distribuerai le code source à la fin du projet .
    FAQ3: J'ai des connaissances, mais aucune (vrai) pratique. Je ne suis pas un pro dans ce domaine, du coup il est fort possible que je fais des erreurs. N'hésitez pas à me critiquer!


    A très bientôt!
    Dernière modification par Louck ; 06/01/2015 à 16h32.

  2. #2
    Yeah ! Super idée ! Je suis en train me faire la main sur unity depuis peu. Au début c'était pour être en mesure d'aider un ami à réaliser un jeu, mais au final je suis devenu fan. J'avais soigneusement mis de coté l'idée de faire du multi pour me concentrer sur le gameplay et l'univers... Du coup ce topic me redonne espoir et je vais le suivre de prêt !!

  3. #3
    Hello,

    Bon courage pour ton projet ! Je te conseil aussi cet article :
    http://www.gabrielgambetta.com/fast_...ltiplayer.html

    J'ai aussi implémenté un mode multi pour mon jeu dernièrement, mais ce n'était pas sous Unity, mais avec LOVE, donc tout en LUA et en utilisant lua-enet. J'avoue que j'en suis plutôt satisfait, j'ai aussi un serveur autoritaire qui peut soit tourner avec un client (dans un thread à part) ou en mode dédié. Par contre je me suis limité à 4 joueurs, il faudrait que j'optimise un peu si je veux faire tourner ça pour 8.

    Et du coup, tu vas aussi faire une partie "lobby" pour gérer une liste de serveurs et mettre en relation les clients ? Il y a des choses dans Unity pour ça ? Si ce n'est pas déjà fait, jette un oeil aux techniques d'UDP Hole Punching, ça risque de t'être utile (ça a été plutôt galère à mettre en place de mon côté...). Je te conseil ce doc sur le sujet :
    http://www.brynosaurus.com/pub/net/p2pnat/

  4. #4
    Citation Envoyé par Fenrir Voir le message
    Hello,

    Bon courage pour ton projet ! Je te conseil aussi cet article :
    http://www.gabrielgambetta.com/fast_...ltiplayer.html
    J'ai déjà lu cet article il y a quelques jours. Et en effet, ca raconte de bonnes choses .

    Pour le reste, je vais surtout réaliser une version naïve de l'architecture client/server, qui fonctionne bien.
    En gros, je commencerai par ca:
    • Etape 1: Connexion client/server (par IP)
    • Etape 2: Envoi données clients
    • Etape 3: Envoi données serveurs (snapshots)
    • Etape 4: Deconnexion


    Mais dans un second temps, je tenterai de peaufiner la chose à coup de Prediction, Interpolation/Extrapolation, Master Server/Lobby, Physique, et j'en passe . Tout ca accompagné de mes commentaires personnels .


    Je te conseil ce doc sur le sujet :
    http://www.brynosaurus.com/pub/net/p2pnat/
    J'ai regardé rapidement, mais j'ai l'impression que ca concerne les communications en P2P. Enfin, je ne vois pas trop l’intérêt d'utiliser ce modèle, surtout si on veut utiliser un serveur autoritaire (EDIT: à part pour faire de la communication textuelle ou vocale ?).

    A voir! Je détaillerai mes pensées au cours de mes mésaventures, et vous pourrez critiquer (ou jeter des tomates) .

  5. #5
    J'ai regardé rapidement, mais j'ai l'impression que ca concerne les communications en P2P. Enfin, je ne vois pas trop l’intérêt d'utiliser ce modèle, surtout si on veut utiliser un serveur autoritaire (EDIT: à part pour faire de la communication textuelle ou vocale ?).
    Ben en fait tout dépend si ton serveur tournera en dédié avec une IP publique ou avec l'un de tes clients (donc potentiellement derrière un NAT). Dans le premier cas effectivement ce papier ne sert à rien, mais si ton serveur est derrière un NAT ça explique comment initier la communication avec les autres clients (et éviter d'avoir à ouvrir manuellement des ports sur ton routeur ou ta box adsl).

  6. #6
    Excellente initiative, je suis aussi en train de tâter le terrain pour une transition vers Unity en ce moment et je vais donc observer tes progrès avec grand intérêt.

  7. #7
    Citation Envoyé par Fenrir Voir le message
    Ben en fait tout dépend si ton serveur tournera en dédié avec une IP publique ou avec l'un de tes clients (donc potentiellement derrière un NAT). Dans le premier cas effectivement ce papier ne sert à rien, mais si ton serveur est derrière un NAT ça explique comment initier la communication avec les autres clients (et éviter d'avoir à ouvrir manuellement des ports sur ton routeur ou ta box adsl).
    Oh, je n'y pensais pas du tout!
    Je prend note pour un futur proche .

  8. #8
    J'ai réalisé un proto de MMO à la fac ya 2 ans, c'était pas la folie mais ça marchait pas mal !
    Bon courage, je suivrais tes avancés

  9. #9
    Oublié de préciser: j'écrirai en tant que développeur, pour les développeurs. Certains termes risquent de ne pas être compréhensibles pour les joueurs (Callback? Fonction anonyme?). Dans le doute, dites le et je ferais un petit lexique.
    Sinon, lets go!


    CHAPITRE 1: UN NOUVEAU PAQUET
    Dans cette première partie, comme je l'ai précisé plus tôt, je vais faire dans le plus basique et le plus naïf. Je vais y fixer les bases de mon architecture, m'y féliciter, avant d'échouer majestueusement dans les futurs chapitres de mes mésaventures.


    Etape 1: Lancement et connexion client/server
    On commence par le plus simple et par le moins intéressant: une simple connexion entre un client et un serveur.

    Mais avant tout, j'ai préparé le terrain en créant plusieurs composants vierges:
    • Composant Server: Contiendra tout le code dédié au serveur.
    • Composant Client: Contiendra tout le code dédié au client/joueur.
    • Composant Shared: Il arrive que certaines fonctions soient communes entre le serveur et le client. Pour éviter les copies, on colle tout ca dans ce composant. Au cas oû!
    • Composant Network Manager: Le chef d'orchestre de tout ce bordel. Il est surtout utilisé pour gérer la connexion et la déconnexion d'un serveur ou d'un joueur.


    J'utiliserai, bien sûr, notre composant Network View qui nous permettra de communiquer avec le serveur ou avec les joueurs de la partie. D'ailleurs, sans ce composant, pas de réseau, pas de multijoueurs.

    Tous ces composants seront utilisés par une seule entité (ou "gameobject", dans le langage d'Unity), qui portera le doux nom de Networking.
    Pour simplifier, toute la partie réseau sera gérée par cet objet.




    Retour avec la connexion des joueurs.
    Je ne me suis pas trop embêté et j'ai honteusement copié une partie du code ici:
    http://www.paladinstudios.com/2013/0...me-with-unity/

    J'ai bien sûr modifié le code à ma sauce. Par exemple, je n'utilise pas le Master Server pour l'instant (je me connecte directement à mon serveur par IP). Et j'ai tout rangé dans le composant Network Manager.


    De façon détaillé, lorsqu'on créé un serveur:
    1. Invocation de la fonction callback OnServerInitialized(). Je charge le composant Server et je démarre tout le bordel...
    2. ... Mais vu qu'il y a rien à faire d'autre pour l'instant... on fait rien d'autre!


    Du côté client, c'est un peu plus compliqué. Quand un client se connecte au serveur, du côté client:
    1. Callback OnConnectedToServer(). J'active le composant Client, et je charge ce qu'il faut.
    2. Et j'attend une réponse du serveur.


    Du côté serveur:
    1. Callback OnPlayerConnected() ("ouai j'ai des amis!"). Je récupère les informations du client dans cette méthode (objet NetworkPlayer).
      A partir de ces informations, j'enregistre le client sur le serveur: Je lui attribue un identifiant et je lui réserve un petit espace de stockage, au chaud, au cas où s'il veut stocker des gameobjects dont il serait propriétaire (ce qui peut être très utile pour plus tard).
    2. Ensuite, le serveur envoie au client son ID (ou identifiant, pour les noobs du fond qui ne suivent pas).
    3. Le client stocke son ID.
    4. The End.




    Ca serait une utopie que le netcode puisse se résumer à ca.


    Pour l'instant, nous avons réussi à lancer un serveur et à faire connecter nos clients sur notre serveur. Youpie tralala. Champagne pour tout le monde.

    La prochaine fois, on entrera dans le vif du sujet: Faire translater les données du jeu entre le client et le serveur.




    En attendant, je vais vous parler de RPC et de "State Synchronization".
    Globalement, sous Unity, il existe deux façons pour échanger les informations sur un réseau:
    • Via le "State Synchronization"-machin. Grosso modo, c'est Unity qui fait tout le travail, et qui translate les données d'un composant. Très utilise pour ne pas s’embêter à gérer la position et la rotation d'un protagoniste.
    • Via le RPC. Grosso merdo, un client invoque une méthode nommée sur le réseau, que les autres clients (et/ou serveur) peuvent intercepter. Le RPC ne fait pas tout, mais permet de translater beaucoup de chose sur le réseau et on a un peu plus de contrôle sur les données. Cette technique est surtout utile pour chatter avec les autres (ou pour insulter, si le jeu est un MOBA), et pour informer certaines actions spécifiques du joueur (tirer, s'accroupir, sauter, etc...).


    Ces définitions, on les retrouve dans les documents d'Unity et dans 90% des tutorials. Ils n'ont pas tort, ces deux méthodes se complètent bien et ont chacun un intérêt.


    Néanmoins, étant un truz rebelz #onlachrine, j'ai décidé de ne pas suivre cet exemple.

    La raison principale est que le State Synchronization est une technique très gourmande en ressource réseau, malgré elle. Je soupçonne Unity d'envoyer trop de données avec cette méthode. En utilisant les RPC, j'arrive à envoyer les mêmes informations, sans faire sauter mon débit. La contre-partie est que je dois tout coder moi même, mais cela a un avantage sur le long terme.
    L'autre point noir est qu'utiliser le SS implique d'invoquer le composant Network View sur d'autres gameobjects (protagonistes gérés par les joueurs, pnj, etc...) et de décentraliser toute notre gestion du réseau. Chose que je ne souhaite pas faire (dans mon cas bien sûr. Gérer le réseau par entité/composant a ses avantages)


    Du coup, afin d'avoir un contrôle total sur le réseau, afin d'envoyer/recevoir des données de façon optimisés, afin de me simplifier la vie, je centralise tout le réseau dans un objet, je n'utiliserai qu'un seul Network View et je ne ferais appel qu'aux RPC.

    Mais peux être que je me trompe.




    PS: Je ne suis pas un fort en écriture. Si vous voyez des erreurs ou des fautes, n'hésitez pas.
    Sinon, si vous avez des questions ou des critiques en attendant, je suis à l'écoute!
    Dernière modification par Louck ; 05/01/2015 à 23h59.

  10. #10
    Du coup niveau bande passante, c'est quoi ton objectif (aussi bien pour le serveur que les clients) ?

  11. #11
    Je sais pas si tu as eu l'occasion de jeter un oeil à TNet, la librairie réseau codée par le type qui à fait NGUI (une des plus puissante lib de GUI pour Unity). Elle fonctionne tout par RPC aussi et tout le code source est dispo si tu as acheté son package. Je m'en étais servi pour faire un petit shooter avec des vaisseaux vus de dessus, avec un chat qui va bien etc.. Hésite pas à m'écrire si tu veux plus d'infos ou y jeter un oeil.

  12. #12
    Du coup niveau bande passante, c'est quoi ton objectif (aussi bien pour le serveur que les clients) ?
    Je reviendrai là dessus dans un prochain chapitre.
    La question des données (ou de la bande passante) dépend du jeu surtout. Le but de l'architecture réseau est de pouvoir optimiser un maximum l'échange de ces données.

    Je sais pas si tu as eu l'occasion de jeter un oeil à TNet
    Je jetterai un oeil à ca à la fin du projet. Mon but est de déjà faire fonctionner l'engin à partir des composantes de bases d'Unity, pour l'instant


    On continue!




    ETAPE 2: Gestion des données sur le réseau du jeu
    En faite, je suis un menteur. Ou un petit menteur, c'est vous qui voyez.

    Avant d'attaquer le transfert des données clients/serveurs, il faut parler de ces fameuses données: Quelles données le client va envoyer au serveur ? Quelle information le serveur va envoyer à ses clients ? C'est bien gentil de parler des échanges de paquets entre deux postes, mais il faut déjà savoir ce qu'il faut mettre dedans.

    Pour ce point là, pas de mystère: ca dépend du jeu - lui-même - et de son game-design.
    Si le jeu est un STR tour par tour, globalement, le joueur informe le serveur des actions qu'il a effectué durant son tour. Dans un FPS en 3D, il faut gérer la position et la rotation du personnage, en plus des actions habituelles dans les jeux de tirs (comme tirer et insulter les mamans des autres).
    Bref. Chaque jeu-vidéo a ses propres règles et ses propres données.



    Pour gérer les règles fixées par un jeu, j'ai créé une interface NetworkGameRules. L'interface contient une liste de méthodes qui décrivent le fonctionnement du jeu sur le réseau: Quelles informations à envoyer, comment les données seront traitées à la réception, qu'est ce qu'il faut faire en début de partie, etc...
    Cette interface sera utilisée par mes composants réseaux (NetworkServer, NetworkClient, ...). Pour cela, j'ajoute un nouveau paramètre à mes composants, qui prendra en compte mon interface.

    Spoiler Alert!
    C'est un peu plus merdique que ca: vu que je ne peux pas passer directement un fichier C# en paramètre, j'ai du créer un gameobjet qui utilise un composant qui utilise mon interface, avant de passer cet objet en paramètre. Je pense qu'il y a une meilleure solution, mais je ne voulais pas me prendre la tête pour le moment.



    Ce fichier nous sauve d'un gros problème. Mais il y en a un deuxième point à régler, avant de continuer: les entités (ou les gameobjects).

    Durant une partie, le serveur doit pouvoir instancier, éditer, supprimer les éléments du jeu, dans son propre contexte et dans les contextes des joueurs. Dans le cas d'un FPS en ligne, genre Counter-Strike, le serveur doit gérer les protagonistes terroristes/antis du jeu, en plus des probables caisses, des armes au sol, des grenades, et de ces cons d’otages.

    Or, il est fort possible que le contexte d'un jeu puisse être différent d'un poste à un autre. Si j'instancie un gameobject sur le réseau (méthode Network.Instantiate()), qui va me garantir que son état sera le même pour tous les clients ? Après que le serveur a créé l'objet, comment il va demander aux clients de modifier cet objet - en particulier - en cours de partie ?
    C'est simple: il faut pouvoir identifier cet objet sur le réseau, et que son ID soit le même et connu de tous.


    Le second point qu'il faut résoudre, avant de s'amuser à envoyer des messages à la gueule du serveur, c'est d'identifier un gameobject dans une partie.

    SI on travaille traditionnellement à base de Network View, il n'est pas nécessaire de définir un ID pour chaque entité: C'est la ViewID du composant qui fait tout le travail (sauf si on s'amuse à le manipuler comme un fou).

    Dans notre cas, il faut définir nous même cet ID, pour chaque entité, après chaque Network.Instantiate(). Etant donné que le serveur est roi, c'est lui qui va définir l'ID pour chaque nouvelle entité, avant de les transmettre aux clients.
    Pour ca, j'ai créé un composant NetworkGameObject qui contiendra un champ ID. Tous les gameobjects qui seront influencés par le module réseau auront ce composant.


    Et là, c'est le drame.




    Il manque quelque chose dans cet alchimie.
    La méthode Network.Instantiate() est une sorte de RPC: elle invoque l'objet sur le poste propriétaire, avant de demander aux autres postes de l'invoquer chez eux.
    De plus, il est très pratique: c'est un RPC bufferisé. Si des joueurs rejoignent tardivement le serveur, ils intercepteront quand même l'appel de cet RPC (et invoqueront lucifer à leur tour... what ?).

    Là ce n'est pas le problème.
    Après que l'objet soit créé, le composant NetworkGameObject va générer un ID du côté serveur, avant de l'envoyer aux clients, dans un autre appel RPC.


    Interrogation!


    Quel composant faut il utiliser, sur un gameobject, pour que son appel RPC fonctionne ?
    Spoiler Alert!
    Un composant Network View



    Etant donné que je souhaite centraliser tous le réseau, pensez-vous que mon gameobject en question possède ce composant ?
    Spoiler Alert!
    NON!

    Spoiler Alert!
    Traduction: j'ai perdu 30 minutes pour trouver ca.





    Du coup, malgré mon effort de vouloir centraliser toutes les échanges réseaux, j'ai du ajouter le composant Network View, en plus du composant NetworkGameObject à mon objet instancié.

    Bon. C'est un mal pour un bien. Avec ce composant en plus, j'ai pu implémenté une gestion automatisée des ID. En ajout, ce composant est très important pour gérer la suppression de l'objet sur le réseau.

    Petite note d'ailleurs: Quand on supprime un objet du réseau, il faut aussi penser à supprimer le fameux RPC bufferisé de notre méthode Network.Instantiate(), pour l'objet en question. Si on ne le supprime pas, ceux qui rejoignent le serveur en cours de partie vont encore créer cet objet (qui est mort).



    Ouf.
    Mais ce n'est pas finis.

    Car créer un gameobject sur le réseau, c'est cool. Mais dans le cas où on créé un gameobject pour un joueur en particulier (un personnage terroriste dans Counter-Strike, par exemple), il faut pouvoir le "lier" au joueur et le gérer quand ce dernier le demande. Son ID ne suffira pas et il faudra le stocker quelque part.

    La chance est que mon serveur réserve déjà une "zone de stockage" (ou une liste) pour chaque client connecté. J'ajoute donc la référence de mon gameobject dans cette liste, pour le client spécifique.


    A partir de là, tout dépend des règles de notre jeu et de comment on va gérer notre objet.



    Comme d'habitude, si il y a des erreurs, n'hésitez pas avec les tomates.
    Dernière modification par Louck ; 08/02/2015 à 20h22.

  13. #13
    Citation Envoyé par lucskywalker Voir le message
    (A propos de TNet) Je jetterai un oeil à ca à la fin du projet. Mon but est de déjà faire fonctionner l'engin à partir des composantes de bases d'Unity, pour l'instant
    Au fait TNet est exactement ce que tu fais une librairie construite sur les fonctions de bases d'Unity. Vu que ton but est d'apprendre en le faisant par toi même c'est parfait, mais si tu sèches sur un concept ou autre ça peut toujours te donner une source d'inspiration.

    Citation Envoyé par lucskywalker Voir le message
    C'est un peu plus merdique que ca: vu que je ne peux pas passer directement un fichier C# en paramètre, j'ai du créer un gameobjet qui utilise un composant qui utilise mon interface, avant de passer cet objet en paramètre. Je pense qu'il y a meilleure solution, mais je ne voulais pas me prendre la tête pour le moment.
    Pour simplifier un peu tu peux passer juste le composant en paramètre plutôt que tout le GameObject, mais tu as peut être mal formulé et c'est déjà ce que tu fais. Sinon tu n'est pas forcé avec Unity d'utiliser que des GameObjects. Tu peux très bien créer une classe C# standard et la passer en paramètre si t'en a besoin, mais comme tu dis c'est pas une priorité pour le moment.

  14. #14
    Très intéressant, merci de prendre le temps d'écrire tout ça lucsk c'est super instructif

  15. #15
    Citation Envoyé par Black Wolf Voir le message
    Au fait TNet est exactement ce que tu fais une librairie construite sur les fonctions de bases d'Unity. Vu que ton but est d'apprendre en le faisant par toi même c'est parfait, mais si tu sèches sur un concept ou autre ça peut toujours te donner une source d'inspiration.
    Ah d'accord.
    Je jetterai un oeil plus tard, ca peut servir . Pour l'instant, j'avance en mode "autodidacte".


    Pour simplifier un peu tu peux passer juste le composant en paramètre plutôt que tout le GameObject, mais tu as peut être mal formulé et c'est déjà ce que tu fais. Sinon tu n'est pas forcé avec Unity d'utiliser que des GameObjects. Tu peux très bien créer une classe C# standard et la passer en paramètre si t'en a besoin, mais comme tu dis c'est pas une priorité pour le moment.
    C'est ca.
    Je sais qu'on peut passer autre chose que des GameObjects en paramètre d'un composant, dans l'éditeur. Mais je ne voulais vraiment pas me prendre la tête sur le moment .


    J'ai corrigé/édité quelques phrases de mon étape 2. J'étais un peu fatigué ce jour là.

  16. #16
    Petit message pour dire que je suis très occupé ce mois-ci, pour travailler sur mon projet technique.
    Je tente de faire avancer mon architecture réseau, mais lentement, afin d'avoir de la matière à présenter .

  17. #17
    Oooohhh que vois-je en ce mois de décembre ?
    Du temps libre !
    Youpie!


    ETAPE 3: L'envoi des paquets clients au serveur
    Maintenant que le nécessaire est en place, c'est le moment idéal de débuter la communication entre notre client et notre serveur chéri.
    Pour le contexte, on va supposer que notre but (enfin, mon but) est de créer le jeu suivant:
    CUBE MMO SIMULATOR

    En gros, le jeu consiste en 3 principes:
    1. Un joueur = Un cube dans le jeu.
    2. Le joueur peut déplacer son cube sur un espace en 2D.
    3. Si le joueur se déconnecte, le cube génère une explosion (parce que https://www.youtube.com/watch?v=v7ssUivM-eM)


    Easy! Commençons.



    Quand le client se connecte au serveur, ce dernier doit générer un cube pour le joueur. Sinon, avec quoi il va pouvoir jouer ? Radin!
    Pour ca, j'ai prévu une méthode abstraite OnClientRegistered() dans mon interface NetworkGameRules. Pour mon jeu, cette méthode va simplement instancier une prefab "cube" sur le réseau (via Network.Instantiate()).
    Ensuite, je précise au serveur que ce nouveau objet est actuellement contrôler par notre joueur. Cette information m'aidera pour la suite (cf étape 2).


    Ca, c'étais la partie la plus facile.
    Maintenant, on va s'amuser.


    L'objectif du client est d'informer le serveur des "commandes" ou actions du joueur.
    Dans l’absolu, le client peut envoyer n'importe quoi au serveur: la position actuelle du joueur, l'animation courante, où il vise, etc etc... Mais il faut faire attention à deux choses:
    • A ne pas trop surcharger inutilement le paquet. Je reviendrai là dessus dans un prochain chapitre sur l'optimisation.
    • Mais surtout: aux tricheurs.


    Il faut savoir qu'il est TRES facile d'envoyer un paquet personnalité au serveur ou autres autres joueurs, sans être un génie en hacking. De même, il n'est pas difficile de lire les paquets qui s'échangent sur le réseau. En lisant ces paquets, il est possible de connaitre certaines informations sur la partie - comme la position des adversaires - et de réaliser un wallhack ou un aimbot.



    La seule solution face à ce problème est de contrôler les paquets (s'ils sont au bon format, si les données sont correctes, ...) et de restreindre les informations à translater. Mais c'est beaucoup plus facile à dire qu'à faire, et les tricheurs trouveront toujours une alternative.


    Cependant, il existe un problème avant de parler d'optimisation et de tricherie: quelles données faut-il envoyer sur le réseau ?

    A un moment donné dans la réalisation du jeu, le game designer doit concevoir des interfaces qui lient le joueur au jeu et/ou à son avatar: le monde à afficher à l'écran, le HUD, les menus... et l'interface d'entrée (ou les contrôles du jeu).
    Pour mon jeu, les commandes sont:
    • Ma souris pour cliquer sur le bouton "Connecter".
    • Les "touches fléchées" pour déplacer le cube.
    • Le bouton "Echap" pour se déconnecter automatiquement de la partie.


    Au niveau du réseau, il n'est pas nécessaire de translater toutes les actions du client. Tout dépend du jeu et des nécessités pour faire fonctionner une partie multijoueurs.
    Par exemple, dans le jeu de carte Hearthstone, il est utile d'informer le joueur adverse de ce qu'on fait avec nos cartes. Mais il n'est pas important de lui informer de la position de notre souris et de ses cliques les bords du plateau.

    Pour mon jeu, c'est très simple: seul les commandes de déplacement du cube et les boutons "Echap" et "Connecter" seront envoyées au serveur.
    Dans l'interface NetworkGameRule, j'ai ajouté deux méthodes abstraites:
    - Du côté client - BuildInputState(): Doit retourner un code qui correspond aux commandes exécutées par le joueur. Si le joueur appuie sur la touche "Flèche haut", la méthode retournera le code 1. Le bouton "Flèche droite" retourne le code 2. Si le joueur appuie sur ces deux boutons, la méthode retournera le code 3 (1 + 2). J'utilise une énumération afin de bien lister les actions et leurs codes.
    - Du côté serveur - ExecuteInputState(): Cette méthode exécutera les fonctions du jeu, selon le code client. Par exemple, si la méthode reçoit le code 1 de notre client, alors elle exécutera la fonction de déplacement du cube, dans la direction "haut".


    Après avoir définie les données, il faut les envoyer.
    La technique la plus connue est d'envoyer les informations à une certaine fréquence, nommée tickrate. Un tickrate à 60 signifie que le client (ou serveur) enverra un paquet 60 fois par seconde. Dans cette configuration, un paquet est envoyé toutes les 16 millisecondes (1000/60). En théorie, plus le tickrate est grand, plus le contexte de la partie sera synchronisée et cohérente pour les joueurs et le serveur, mais plus la bande passante sera utilisée.
    Dans mon architecture, la méthode Update() contient le code nécessaire pour l'envoi des données, du côté client et et du côté serveur. La fréquence d'exécution de cette méthode est proportionnelle au tickrate.

    Dès que le client peut envoyer les données, il génère un code avec la méthode BuildInputState() et il l'envoie au serveur via un appel RPC: networkView.RPC("MethodeDuServeur", RPCMode.Server, code).
    Du côté serveur, après avoir contrôlé le paquet reçu par l'appel RPC, il exécute la méthode ExecuteInputState() avec le code du client et met à jour sa partie.
    Et c'est finis!
    Tout fonctionne!
    Le cube se déplace selon la volonté du joueur sur l'écran du serveur!


    The next gen is here.


    Il y a quand même un petit problème sur ma façon de procéder.
    Actuellement, le client informe le serveur des commandes qui sont exécutées par le joueur. Mais quid des commandes un peu plus exotiques, autres que des boutons ? Comment informer le serveur que le joueur a déplacé sa souris de 10cm ? Ou qu'il utilise le joystick de sa manette xBox?
    A mon avis, la solution serait de faire un second appel RPC, qui informera le serveur de ces commandes supplémentaires, qui ne peuvent être traduites par la méthode BuildInputState().
    Mais nous reverrons cela dans un prochain chapitre.



    Je m'attaque à la prochaine étape, très bientôt .
    Dernière modification par Louck ; 06/01/2015 à 16h29.

  18. #18
    C'est quand "très bientôt" ?
    Merci en tout cas, ce que tu décris est très intéressant.

  19. #19
    Dès que je trouve le temps, pour ne pas mentir. J'essaye de faire la suite la semaine prochaine .

  20. #20
    ETAPE 4 : L'envoi des paquets serveurs aux clients

    L'envoi des informations du client au serveur est simple. La réciproque n'est pas non plus compliquée.
    Techniquement, c'est la même chose: le serveur construit un paquet à partir de ses données, et l'envoi à tous ses clients via un appel RPC. Les clients liront le paquet et mettront à jour leurs contextes.
    La seule différence avec l'envoi client => serveur, c'est les données à envoyer.

    Jusqu'à maintenant, le serveur reçoit toutes les commandes des clients et met à jour son environnement. Le serveur est celui qui possède le contexte le plus à jour. Ainsi, son objectif est d'informer les clients de l'état de la partie à un instant T: Il envoi donc un snapshot de son contexte.

    Spoiler Alert!
    On peut aussi parler de snapshot du côté client: il envoi un "état" de ses actions au serveur. Durant tout le jeu, seul les snapshots clients et serveurs sont translatés sur le réseau, à l'exception pour la connexion/déconnexion/instanciation. Durant tout le jeu, les clients et les serveurs s'informent, sans repos, de l'état de leurs contextes. Je viens de le remarquer maintenant. J'aurais du parler de ca depuis le début. My bad.




    La question est de savoir ce que va contenir le snapshot serveur, ou de comment traduire le contexte d'un jeu en données.

    Comme je l'ai répété à plusieurs reprises dans les précédentes étapes, les données dépendent du jeu lui-même. Or, je cherche à réaliser une architecture qui peut être réutilisée pour de nombreux jeux... tant que c'est réalisé via Unity. Du coup, je dois trouver un moyen pour traduire le contexte d'une scène sous Unity, en données.


    Une scène d'Unity est composé d’entités (ou de gameobjects). Dans l'étape 2, j'ai pu générer un identifiant réseau pour certaines entités du jeu. L'idée serait de récupérer les informations de chacune de ces entités identifiées.
    Pour mon jeu, les cubes se déplacent et changent de position. Seule cette information est importante.
    Pour d'autres jeux, la position des entités n'est pas exclusivement importante. Dans les jeux en 3D, la rotation des entités est très utile. Sur certains FPS et RPG, connaitre l'équipement que porte les personnages est essentiel.

    Plus l'entité possède de nombreux états, plus il y aura de données à gérer, plus le snapshot sera important, et plus le paquet sera... gros. Mais c'est normal: c'est le serveur qui gère tout le réseau de la partie. Il est responsable et doit posséder une bonne connexion pour échanger les données avec TOUS les joueurs de la partie. Contrairement au client qui ne communique qu'avec le serveur.
    Pour autant, il ne faut pas trop surcharger le paquet. Mais nous verrons ca dans un prochain chapitre, sur l'optimisation. Pour l'instant, je reste simple et naïf sur la réalisation de mon architecture multijoueurs.



    Bref!

    Pour mon jeu, pour chaque cube, je récupère leurs identifiants et leurs positions via ma méthode abstraite BuildGameObjectSnapshot(), dans l'interface NetworkGameRules. Je ne m’embête pas, je traduis toutes ces données au format texte avec un séparateur (qui doit être un caractère très unique, et qui ne doit pas se confondre avec les autres caractères utilisées par la donnée utile. Genre le code 254 ou 255 de la table ASCII).
    Le résultat est une longue chaîne de caractères qui sera mon snapshot. Et donc mon futur paquet.

    Spoiler Alert!
    Précision: un snapshot n'est pas un paquet. Dans mon cas, un snapshot, c'est une donnée. Un paquet, c'est un "conteneur" de données, dans le cadre d'une transmission sur le réseau.
    Je le précise au cas où il y a des non-développeurs qui me lisent .


    Après avoir récupéré le snapshot via un appel RPC du serveur, le client le découpe en plusieurs "mini-snapshots" (selon le caractère séparateur défini). Enfin, pour chaque entité du jeu et son mini-snapshot dédié, il fera appel à la fonction abstraite ExecuteGameObjectSnapshot() qui appliquera les modifications d'états sur l'entité en question.
    Dans mon jeu, le mini-snapshot contient la nouvelle position du joueur. La méthode va simplement déplacer mon cube à sa nouvelle position.


    Enfin! Le contexte du client est synchronisé sur celui du serveur. Je peux voir mon cube bouger sur mon écran de joueur ...




    ... à quelques millisecondes prêts . Merci au temps de latence, le plus grand fléau du jeu-vidéo.




    Dans la technique, tant que nous maîtrisons l'envoi des données du client au serveur (étape 3), la technique est identique pour la situation inverse.
    Néanmoins, le plus grand défi du jeu en réseau se réside dans la donnée - savoir ce qu'il faut envoyer et ce qu'il ne faut pas envoyer.
    Ca, et le ping.
    Et les tricheurs.


    L'essentiel est en place. Il ne manque plus que la... déconnexion!
    La prochaine étape sera très courte. Mais j'en profiterai pour en faire le point, et pour planifier le second chapitre qui sera.... très intéressant.
    Dernière modification par Louck ; 08/02/2015 à 20h22. Motif: Précision

  21. #21
    Salut !

    M'étant déjà intéressé au développement de jeux vidéos, je m'étais bien entendu un peu penché sur le fonctionnement d'une architecture client/serveur.
    Cependant, j'étais (et je suis toujours) loin d'être un bon développeur. Mes seules expériences étant de la prog en école d'ingé ou du dev de sites webs, je me suis heurté à pas mal de problèmes de compréhensions...

    Et là je suis tombé sur ton topic qui est TRES intéressant !
    Merci beaucoup de partager tout ça avec nous !

  22. #22
    Merci pour les encouragements .

    Pour l'instant, ce chapitre n'est qu'une mise en place du réseau. C'est la version bête et logique de comment les joueurs peuvent communiquer entre eux.
    Les prochains chapitres vont être un peu plus technique.

    Je prévois un chapitre sur l'optimisation du réseau (Unity fait déjà un gros travail, mais je vais quand même faire mumuse avec Wireshark). Et un autre - sûrement le plus intéressant - sur la gestion de la latence dans les jeux (Prediction, Interpolation/Extrapolation, etc...).

    Quand l'architecture sera finis, je vais peux être réaliser un mini-jeu d'action multijoueurs, jouable sur le web, pour que tout le monde puisse tester .

  23. #23
    ETAPE 5 : La déconnexion du client (et aussi du serveur, mais on s'en fout de lui)

    La déconnexion est l'inverse de la connexion: au lieu d'instancier les éléments, nous les détruisons. C'est la partie la moins complexe à gérer. Ca se résume à:
    • Si le client se déconnecte, nettoyer la partie de toutes ses traces.
    • Si le serveur se déconnecte, "finaliser" la partie et débrancher tous les clients.


    Spoiler Alert!
    Pour cette étape, on va surtout parler de la déconnexion cliente: La déconnexion du serveur est bien gérée par Unity et invoque automatiquement le callback OnDisconnectedFromServer(). A ce moment là, on peut demander aux clients rejetés de revenir à la page d'accueil. Pas de problèmes .



    Du côté de la déconnexion cliente, l'unique difficultés est de gérer le "lien" que possède les objets du jeu avec le joueur. Si un objet est lié au joueur (car créé et/ou géré par lui) mais est nécessaire au déroulement de la partie, faut-il le garder ou le supprimer ?

    Exemple très classique: La joueuse Alice tire une balle en direction de son adversaire Bob. Faute de posséder une FAI de merde, Alice subit une coupure réseau et se déconnecte - soudainement - de la partie. Est-ce que la balle tirée est supprimée de la partie (car dépendante de la tireuse) ou doit-elle continuer son chemin et atterrir dans le corps de Bob ? Si la balle touche sa cible, quel score allons-nous afficher ? "NULL a tuer Bob" ?
    Prenons un autre exemple: Carol construit une maison dans le jeu très populaire MonCraft. Carole possède le même FAI qu'Alice, et subit - elle aussi - une déconnexion forcée. Est-ce que la maison construire par Carol doit être détruite suite à sa déconnexion ?


    Selon le gamedesign, soit on garde les entités gérés par les joueurs, soit on les détruit. La solution la plus simple, mais pas forcement la meilleure, est de tout supprimer.
    Si la suppression n'est pas la réponse, alors nous devons rendre ces objets persistants: Créé et/ou gérer par un joueur, mais qui peut fonctionner sans lui. Ces entités n'ont pas de vrai liens avec le joueur, mais ont un simple mémo "Créé par Carol". Ces éléments sont dépendants de la partie et non du joueur.

    D'oû le terme "Monde persistant" dans les MEUPORG.



    La persistance n'est pas difficile à gérer. L'objectif est de simplement dissocier les objets des propriétaires et de les faire fonctionner avec et sans les autres joueurs. Mais il ne faut pas non plus tout séparer: Certaines entités sont très dépendantes de leurs auteurs et leurs suppressions peuvent être préférables... dont la balle tirée par Alice.
    Pour conclure: tout dépend de la conception du jeu, ou de son gamedesign. Encore .


    Pour mon jeu, je souhaite supprimer les cubes et générer une explosion à leur disparition. Je met en place la solution suivante:
    - Quand le joueur se déconnecte du jeu (brusquement ou via Network.Disconnect()), le callback OnPlayerDisconnected() est invoqué du côté serveur.
    - Je supprime toutes les données et instances liées au client (dont le cube) et son espace de stockage. Il existe une jolie fonction pour détruire les gameobjects sur le réseau: Network.Destroy()
    - Je vide tous les RPC bufferisés du client. Seul le RPC "Création du cube" est bufferisé. Sa suppression est nécessaire pour empêcher la création d'un cube "fantome" quand de nouveaux joueurs rejoindront la partie.
    - J'informe la disparition du cube aux autres joueurs et je génère une explosion à sa position.

    Actuellement, le seul moyen que j'ai pour informer les autres clients de la déconnexion de notre joueur aimé, c'est via un snapshot du serveur. Mais étant donné que cela arrive que dans une situation assez unique (le joueur se connecte et se déconnecte qu'une seule fois durant sa session) je trouve plus intéressant d'envoyer cette information via un autre appel RPC, sans surcharger le snapshot.
    Pour cela, j'ai créé les méthodes SendRawData() et ReadRawData() pour les clients et le serveur (plus techniquement, dans la classe SharedNetwork). Via ces fonctions, je peux envoyer n'importe quelle donnée aux membres du réseau, que ce soit un message ou une action spécifique. Comme la déconnexion d'un joueur.

    De cette façon, quand le client se déconnecte de ma partie, le serveur envoie un message "explosion" aux autres clients (SendRawData()). A sa lecture (ReadRawData()), le client générera un effet graphique - une explosion de pixels - à l'emplacement du cube disparu.


    Et c'est sur ce dernier point que je finis la réalisation de la première version de mon architecture multijoueurs. Hallelujah!



    Mais ceci n'est que le premier pas. La réalité est qu'il reste encore beaucoup de choses à gérer, dont les problèmes de latence, la physique du jeu et les pertes de synchronisations. Au mieux.

    On en reparlera de tout ca... Dans un nouveau chapitre .



    Désolé pour le temps que j'ai mis pour publier toutes ces étapes. Etant bien occupé en ce moment, il me faut plus de deux semaines pour avancer ce projet ET pour mettre à jour le devlog. D'ailleurs, je vais avoir besoin d'un peu plus de temps pour préparer le prochain chapitre.
    J'ai quand même pour objectif de finir ce bousin pour cette année, avec un mini-jeu en prime.

    Le premier obstacle est passé, le plus fun arrive .




    See you soon!
    Dernière modification par Louck ; 27/01/2015 à 15h48. Motif: Précision

  24. #24


    Merci pour ces retours, à chaque nouvel épisode c'est un réel plaisir.
    Je touche pas du tout à Unity ('fais de la prog par ailleurs) et justement c’était un sujet sur lequel je me posais pas mal de questions !

    J'attend le suivant avec impatience, hâte de voir la suite

  25. #25
    Je pense que pour gagner en visibilité, tu devrais faire des liens vers chacun des chapitres dans ton tout premier post !
    Merci beaucoup en tout cas !

  26. #26
    J'y pense à faire un sommaire dans le premier post de ce topic. Je le ferais en même temps que l'introduction du chapitre 2 .

    Chose intéressante en passant, la couche transport personnalisé d'Unity gère beaucoup de choses... tout en étant UDP. En gros, ils font le gros du travail pour l'optimisation des transferts des paquets.
    Mais il n'y a pas que ca que je peux optimiser .

  27. #27
    CHAPITRE 2: LA MENACE DU LAG

    Nous avons mis en place une architecture multijoueurs classique. Rien de fantastique, à l'exception du fait que je n'utilise que les RPC pour faire fonctionner le bousin. Techniquement, ca marche. Mais elle peut être améliorée sur plusieurs points. D’où ce chapitre, sur l'optimisation des paquets.


    Etape 1: Le calme avant la tempête

    Avant de continuer, j'ai du modifier certaines éléments sur mon jeu de cube. Contrairement à ce qui était marqué dans le premier chapitre, tout n'était pas parfait. En résumé:
    • Lorsque le serveur ou le client se déconnectait de la partie, les entités du jeu étaient toujours présentes sur le poste du client. La solution est de tout nettoyer... ou de recharger simplement la scène.


    • Il n'y avait aucune indication pour que le joueur repère son propre cube. Mon but est d'afficher le cube du joueur propriétaire en rouge, mais uniquement sur son écran. Il me fallait donc une méthode pour exécuter du code côté client (au niveau de l'interface).
      J'ai alors modifié le composant NetworkGameObject qui servait jusque-là à gérer les identifiants des objets sur le réseau. Je lui ai ajouté un nouveau paramètre "script". Ce script s'exécutera seulement si le joueur est propriétaire de l'objet en question et uniquement sur le poste du dit joueur (les autres ne verront pas les changements).
      J'ai rapidement créé un petit script
      Spoiler Alert!
      (plutôt un composant, pour simplifier le truc)
      qui change la couleur du cube en rouge. Si le joueur est propriétaire du dit cube, ce dernier sera peint en rouge.
      J'en ai profité pour rajouter un autre paramètre script, mais du côté serveur. Peux-être que j'améliorerai ce système plus tard.


    • L'entrée utilisateur (clavier, souris, joystick...) est testée uniquement à la génération du snapshot. En gros, le joueur a un délai extrêmement court, à une frame prêt (= quelques millisecondes) pour que ses commandes soient traitées par ma fonction. En-dehors de ce laps de temps, ca ne fonctionne pas.
      Sur Unity, le buffer d'entrée n'est géré que nativement, nous n'avons pas accès. Du coup j'ai du le gérer à ma façon, via une méthode qui s'exécute à chaque frame (Update()) qui met à jour une liste d'entrée (le buffer) qui sera lu au moment de la génération du snapshot client.


    • Le déplacement du cube était dépendant du tickrate: J'appliquais l'action de mouvement (Transform.Translate()) à la lecture du snapshot. Avec un tickrate à 100 c'est assez fluide. Mais avec un tickrate à 1 ?
      Ma solution est d'utiliser une sorte d’interrupteur, qui change son état activé/désactivé à la lecture du snapshot client par le serveur. Dans la méthode Update() du cube, tant que l’interrupteur est activé, le serveur déplace l'entité dans la direction définie par le joueur.


    Il y a eu d'autres broutilles, mais ce n'étais rien comparé à tout ca.



    Aujourd'hui, mon simulateur de cube marche à merveille . Mais ce n'étais pas compliqué, vu le peu de données à gérer sur le réseau. Si je devais réaliser un jeu beaucoup plus important, avec de la 3D, une bonne IA et une gestion de l'inventaire, ca ne serait pas aussi facile.


    Dans ce chapitre, nous allons toucher à la forme de la donnée et sur comment elle est lu sur le réseau. Je vais optimiser tout ce bordel.
    Par contre, il faudra considérer que n'importe quel jeu peut translater n'importe quelle donnée sur le réseau, sans perdre le moindre détail: il faut préserver le "fond" de la donnée (= l'action de se déplacer, d'utiliser un objet, d'attaquer un monstre, etc...).


    Unity se charge déjà du gros du travail avec l'API RakNet. En utilisant le protocole de transport UDP, l'API ajoute une couche de contrôle pour s'assurer que les paquets arrivent "sans défauts" à destination (= gestion du packet loss). La bibliothèque comporte d'autres fonctionnalités comme la compression des données ou le cryptage des paquets.

    Néanmoins, cette API est utilisée nativement dans le framework Unity et elle n'est pas accessible pour les développeurs. Il nous ai impossible de la reconfigurer à notre besoin. Il y a aussi un manque de documentation sur certains détails techniques, que ce soit du côté de RakNet ou d'Unity.


    Ainsi et pour le fun, j'ai décidé de sortir mon joujou Wireshark pour voir ce que cache tous ces paquets qui sont échangés sur le réseau .

    Ci-dessous, l'échange client (port 57688) et serveur (port 2500), suite à un appel RPC avec un paramètre message "a".


    Plus en détail:


    Comme on peut le voir, la partie Data (= données, pour les noobs de l'anglais) n'est pas lisible. Est-ce lié à la compression de données ? A l'encryption ? Mystère!



    J'ai analysé les échanges de paquets, pour mon jeu de cube. J'ai pu notifier certaines choses:
    • La taille de l'en-tête de ces paquets est de 28 octets (20 octets pour le protocole réseau IP et 8 octets pour le protocole transport UDP, normal). Pour la partie Data, on retrouve la couche RakNet qui réserve 28 octets au minimum. Le reste du Data correspond aux informations de l'appel RPC (dont le nom de la fonction) et à ses paramètres s'il y a. En invoquant mon RPC sans paramètres, le paquet pèse au total 69 octets (dont 13 octets de "données utiles").
    • A la réception du paquet, le destinataire émet un paquet de 43 octets (15 de Data) pour l'envoyeur. Sûrement un paquet ACK (ou "accusée de réception"). Ou pour lui dire Merci et que sa famille va bien imothep.
    • En dehors de ces appels RPC, le framework translate beaucoup de paquets (environ une dizaine) entre le client et le serveur, toutes les 5 secondes en moyenne. Cette fréquence semble varier selon l'activité du réseau. Malheureusement, je n'ai aucune idée de ce qu'ils transmettent :/.
    • La compression des données semble se perfectionner sur le temps, selon la documentation RakNet. Les informations de compression sont translatés sur le réseau. Mais comment Unity gère ca ? I dunno lol! Pour l'instant ce que je sais, c'est que la compression n'est pas "parfaite" dans les premières échanges. Je dois le retester sur la durée.
    • Une partie de la Data (dans la couche RakNet ?) est réservée aux informations d'encryption et de compression. Sa taille est proportionnelle à la taille de la donnée utile.
    • Les statistiques affichées par Unity n'indique que la taille de la partie Data (couche RakNet compris), sans compter celle des en-têtes (IP+UDP). Pour un manque de précision de 28 octets, on ne va pas se plaindre .


    Fâcheusement, sans documentation, je n'en sais pas plus sur les données transmissent par tous ces paquets. J'ai pu trouver des détails concernant la couche RakNet ici mais c'est tout :/. Il est possible que je raconte des conneries aussi, mais tout me semble logique.

    Néanmoins, je peux conclure sur une chose.
    Oû l'optimisation aura le plus d'impact, ce n'est pas sur la taille du paquet que nous transmettons, mais sur la fréquence des échanges clients/serveurs. On le néglige beaucoup, mais ce qui est le plus coûteux dans le transfert du snapshot client, ce n'est pas la donnée (4 octets dans mon cas) mais les en-têtes du paquet et la couche RakNet (56 octets minimum).
    Or, il ne faut pas restreindre le tickrate, au risque de rendre le jeu moins fluide. Au mieux, il n'est pas important d'avoir un tickrate supérieur à 100 paquets/secondes (sauf pour les PGM qui ont des réflexes surhumains). Mais cela dépend des besoins du jeu: est-ce qu'il est nécessaire de mettre à jour aussi fréquemment le contexte de la partie ?



    De même, cela ne veut pas dire qu'il ne faut pas optimiser la taille des données. Certains joueurs ne possèdent pas la fibre, ni une connectique ADSL 100% optimale (comme moi). Un peu de respect pour eux (et pour moi ).


    Au final, je peux optimiser deux choses:
    • La forme du snapshot: Que nous évitions d'envoyer 1MO de data par paquet.
    • La fréquence d'envoie de ces snapshots: Pour ne pas envoyer des paquets qui servent à rien aux autres.



    Ainsi, nous débutons le sujet par la révision de la forme des snapshots client et serveur .
    See you soon!



    Petite précision: Durant ces tests, je n'utilise pas la toute dernière version d'Unity. Récemment la société a prévue de revoir toute l'API réseau et il y aura sûrement des modifications. Je n'en sais pas plus, malheureusement.

    Je vais essayer d'accélérer la cadence de mes posts. J'ai bien dis "essayer". Je veux finir mon architecture (avant d'attaquer les "services", comme le masterserver) au maximum fin août, afin d'avoir le temps pour réaliser un mini-jeu pour la fin de l'année.
    Dernière modification par Louck ; 07/03/2015 à 10h43.

  28. #28
    Note en passant: dans les prochaines versions d'Unity, ils vont mettre ne place leur propre framework réseau, sous le doux nom d'Unet.
    Aucune idée s'ils vont remplacer le fonctionnement des State Synchronization ou des RPC.

    En attendant, je reste sur l'ancienne version d'Unity. Quand le nouveau API sortira, je ferais un mini-chapitre là dessus .

  29. #29
    Etape 2: Un vrai snapshot

    Dans le premier chapitre, je m'amusais à envoyer des valeurs entières ou des chaines de caractères sur le réseau, dans l'espérance d'avoir bien optimisé le système.
    Après vérification, c'étais de la merde.


    Aujourd'hui, nous allons faire un snapshot "propre". Ca ne sera plus une suite de chiffres ou de caractères pas très lisibles. Ca sera un objet composé de champs, chacun représentant une donnée du jeu (position, rotation, etc, vous aurez compris). Je cherche à rendre beaucoup plus simple la gestion des snapshots dans mon architecture. Nous allons utiliser des objets.

    Je vais passer de ca:
    "0.7|0.4"

    à ca:
    class SnapshotClient{
    float inputX = 0.7f;
    float inputY = 0.4f;
    }



    Le problème d'utiliser des objets ou des structures pour représenter les snapshots, c'est de trouver un moyen pour les translater sur le réseau. Nous pouvons sérialiser l'objet en question (avec l'annotation [Serializable]) mais le résultat peut être... effrayant.



    Vous voyez ma classe SnapshotClient ? Sa sérialisation via BinaryFormatter a générée 323 octets.
    323 octets!
    Juste pour savoir si le joueur se déplace horizontalement et/ou verticalement!

    Pour faire des sauvegardes sur son poste, ce n'est pas un problème. Mais pour du Networking, il faut éviter cette technique.


    Plan B... Et si je ne sérialisais pas l'objet mais plutôt ses champs ? La seule règle serait que les champs soient de type primitifs.
    Je réalise ma petite méthode qui lit les champs de l'objet (via FieldInfo), je vérifie si chacun de ces champs possède une valeur, je leur attribue un identifiant pour la futur désérialisation, et j'enregistre le tout dans un tableau d'objet.

    Je sérialise le tableau.. et j'obtient 51 octets. C'est mieux, mais ce n'est pas satisfaisant.
    Spoiler Alert!
    Je pense qu'il existe une meilleure alternative que d'utiliser un tableau d'objet pour cette situation. Mais je ne me suis pas plus creusé la tête



    Plan C.... je fais quelques recherches sur le net. Et je suis tombé sur ca:
    Protocol Buffers

    Je tente le coup!
    J'ajoute les annotations ProtoContract et ProtoMember à mon objet SnapshotClient, je modifie un chouia ma méthode pour sérialiser et désérialiser. Et je test.
    Je sérialise l'objet et j'obtiens une suite de 10 octets! C'est déjà beaucoup plus intéressant.

    J'en ai profité pour un autre test: J'ajoute un nouveau champ "keyCode", de type entier, à ma classe SnapshotClient. Avant la sérialisation, seul ce champ possède une valeur (les autres sont définies à NULL).
    J'exécute ma méthode sérialisation et je reçoit.... 2 octets! Excellent!

    En testant un peu plus, cette bibliothèque se révèle être un bijoux. Elle est très performante et elle cherche à s'adapter aux données qu'on lui donne . C'est LA solution à mon problème se sérialisation.


    Maintenant, il faut mettre en place un vrai système de snapshots.
    Je créé donc deux interfaces et une classe:
    • ISnapshotClient: Contient les instructions et inputs du client, à envoyer au serveur.
    • ISnapshotGameObject: Un snapshot pour chaque élément du jeu, contenant son état (sa position, son équipement, etc...).
    • SnapshotWorld: Un "container" de ISnapshotGameObject, qui sera envoyé au client.


    Chacune de ces interfaces contient les méthodes abstraites suivantes:
    • Reset(): Met à NULL les champs de l'objet (exemple, inputX = NULL).
    • Update(): Met à jour les champs de l'objet par rapport à l'environnement du jeu (inputX = 10).
    • Execute(): Exécute X ou Y actions selon les valeurs des champs (le personnage se déplace de [inputX] case horizontalement).


    Aussi, j'ai revu les fonctions de la classe NetworkGameRule. Je lui ai rajouté les méthodes MakeSnapshotClient() et MakeSnapshotGameObject() qui retournent une nouvelle instance des interfaces ISnapshotClient et ISnapshotGameObject.

    Le fonctionnement des nouveaux snapshots est les suivant:
    Après avoir créé une nouvelle instance du snapshot, mon client/serveur fait appel à sa méthode Reset() et Update() pour le mettre à jour avec le contexte de la partie, avant de le sérialiser en byte[].
    Ensuite je métamorphose le résultat en String car les appels RPC n'acceptent pas les byte[] [spoiler]dans ma version d'Unity[/b] et je le transfère aux autres participants de la partie.
    Enfin, l'opération inverse se produit chez le récepteur du paquet: Je convertie le paquet String en byte[], je le déserialise pour obtenir mon snapshot, et je fais appel à sa fonction Execute(). Fin!

    Dans le cas du SnapshotWorld du côté serveur, la procédure est la même, à l'exception que je créé une instance ISnapshotGameObject pour chaque entité de la partie, avant de tous les stocker dans mon objet SnapshotWorld. Ce sera ce dernier qui sera sérialisé et translaté sur le réseau.


    En théorie, tout doit fonctionner jusqu'à là.












    Et bien non.



    Vu le nombre de messages rouges dans mon débogueur, je vais faire une simple liste des "oublis" qu'il y a eu dans mon code.
    Tout d'abord, il fallait bien annoter les classes (ProtoContract + ProtoMember) ET ses interfaces.
    Pour les listes (pour SnapshotWorld), il faut ajouter l'attribut OverwriteList=true dans l'annotation. Et si le type d'objet utilisé par la liste n'est pas de type primitif, il faut l'annoter. Lui aussi.

    Pour continuer, idiot comme une machine, le sérialiseur ne fait pas le lien entre mon objet instancié SnapshotClient et l'interface qu'il hérite ISnapshotClient. Il faut informer le sérialiseur que mon objet est un "sous-type" de mon interface. Un petit tour de magie - RuntimeTypeModel.Default.Add(typeof(ISnapshotGameO bject), true).AddSubType(1, typeof(TestSnapshotClient)); - et c'est repartie!


    Pour finir, il reste la conversion byte[] en String qui n'était pas correcte. Je fais un new string(byte[]);. Oui, je suis une brute. Mais je le vaux bien.

    Voyez-vous le problème ?

    Le problème est très amateur: Je n'encode pas mon paquet. Un paquet non encodé peut être "illisible" pour les autres machines du réseau. Pire encore, il peut être mal interprété par certains protocoles réseaux. Dans ce dernier cas, je suis certain que le paquet n'arrivera jamais à destination.
    Pour résoudre ce défaut, je reste simple et j'encode ma suite d'octets en Base64 (via Convert.ToBase64String()). Je suis sûr que je n'aurais jamais de problèmes avec ce type d'encodage .






    Et. Ca. MARCHE!


    Je parle beaucoup pour juste expliquer que j'ai créé des classes et que j'utilises Protobuf.
    Avant de poursuivre avec la prochaine étape, je vais mesurer la consommation moyenne de la bande passante, en local du côté serveur, avec un tickrate à 100:



    D'un premier coup d'oeil, nous pouvons dire que la consommation de la bande passante est assez élevée, pour si peu de données et avec un seul joueur en partie.
    Cependant, il faut prendre en compte que la couche RakNet ajoute environ 28 octets dans chaque paquet et qu'il n'y a pas que des appels RPC dans ces échanges (ACK, heartbeat, et j'en passe). Même si ces informations représentent la "réalité" d'une partie multijoueurs, elles cachent la vrai donnée que je veux mesurer: la donnée utile.
    J'ai fixé un tickrate très haut, ce qui est aussi la cause d'une bande passante plus élevée. Or pour les tests de ce chapitre, à mon sens c'est l'idéal.

    Je modifie légèrement ma classe NetworkServer pour qu'il puisse m'informer du nombre d'octets utiles que le serveur envoie et reçoit.



    Les valeurs sont en octets et sont réinitialisés toutes les secondes:
    - "Send" correspond aux nombres d'octets qui sont envoyés en une seconde.
    - "Receive" correspond aux nombres d'octets qui sont reçus en une seconde.
    - "par packet" correspond à la moyenne d'octets envoyés par paquet.
    - "nb appels" correspond au nombre d'invocation RPC.

    Maintenant, avec 4 clients + 1 serveur.



    Première chose qu'on peut remarquer, c'est qu'il y a en moyenne 60 invocations RPC avec un tickrate à 100. Le problème vient de ma méthode Update() des classes NetworkClient et NetworkServer qui sont synchronisées au nombre de frames . A voir plus tard si cela passe mieux avec un thread dédié.

    Sinon, la taille des paquets reste assez élevée. En fouillant un peu plus, j'ai vu que l’encodage en Base64 a doublé la taille de mon snapshot client. De plus, Protobuf ne semble pas vouloir optimiser les listes d'éléments (probablement pour respecter la structure) ce qui n'est pas pratique quand il y a un certain nombre de joueurs en partie.

    Avec ce jeu de données, le snapshot du chapitre 1 (envoi d'un entier ou d'une suite de caractères) est la meilleure solution, même si elle n'est pas maintenable. Il faudra essayer avec beaucoup plus de données ou dans un vrai contexte de jeu.
    Mais pour l'instant, je continue avec cette nouvelle solution qui reste très pratique... surtout pour la suite .


    La prochaine étape sera courte mais concernera la gestion des snapshots par clients et par entités. Ensuite, nous attaquons l'optimisation des paquets .



    EDIT:
    En faite, je dis que les prochaines étapes seront plus courtes mais c'est l'inverse .
    Désolé pour les fautes d'orthographes, s'il y a.
    Dernière modification par Louck ; 15/03/2015 à 20h08.

  30. #30

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
  •