# Jeux vidéo > Jeux vidéo (Discussions générales) > Le coin des développeurs >  Unity Networking, The Experience en 3D

## Louck

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  ::trollface::  ).


Mais, étant fou et avec du temps de libre à perdre, je vais tenter cet aventure  ::lol:: . 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  ::ninja:: ).

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!

----------


## yourykiki

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 !!

----------


## Fenrir

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/

----------


## Louck

> 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 clientsEtape 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)  ::): .

----------


## Fenrir

> 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).

----------


## Grosnours

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.  :;):

----------


## Louck

> 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  ::): .

----------


## Metalink

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  ::):

----------


## Louck

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:
Invocation de la fonction callback *OnServerInitialized()*. Je charge le composant *Server* et je démarre tout le bordel...... 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:*
Callback *OnConnectedToServer()*. J'active le composant *Client*, et je charge ce qu'il faut.Et j'attend une réponse du serveur.

*Du côté serveur:*
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).Ensuite, le serveur envoie au client son ID (ou identifiant, pour les noobs du fond qui ne suivent pas).Le client stocke son ID.*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!

----------


## Fenrir

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

----------


## Black Wolf

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.

----------


## Louck

> 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.

----------


## Black Wolf

> (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.




> 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.

----------


## Hideo

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

----------


## Louck

> 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  ::P: .


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

----------


## Louck

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  ::): .

----------


## Louck

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:
Un joueur = Un cube dans le jeu.Le joueur peut déplacer son cube sur un espace en 2D.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  :;): .

----------


## Rodwin

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

----------


## Louck

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

----------


## Louck

*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.

----------


## Zouhh

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 !  ::):

----------


## Louck

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  ::): .

----------


## Louck

*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  :tired: .


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!

----------


## Hideo

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  :;):

----------


## Zouhh

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 !

----------


## Louck

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  :;): .

----------


## Louck

*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  ::ninja:: .

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  ::P: .

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  :Emo: ).


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.

----------


## Louck

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  ::): .

----------


## Louck

*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!  ::lol::  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!  ::lol::   ::lol::   ::lol:: 

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  ::o: .
Désolé pour les fautes d'orthographes, s'il y a.

----------


## Hideo

Cette joie à chaque nouveau chapitre  ::lol::

----------


## Louck

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  ::P: .





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!

----------


## Louck

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 ?  ::P: .

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!  ::lol:: 


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  ::P: .

----------


## Grhyll

(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 !)

----------


## Louck

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 ?  ::unsure:: 

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.  ::lol:: 
Cependant, cela impliquerai de réinventer la roue et je n'ai pas forcement le temps de tout recoder  :tired: .


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:
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".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.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.8koUpload: 3.3ko

*State Synchronization*Download: 1.8koUpload: 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.2koUpload: 13ko

*State Synchronization*Download: 7.2koUpload: *20.4ko*

What.
The.
Fuck  ::O: 

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.

----------


## Louck

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.

----------


## Hideo

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  ::):

----------


## Ifit

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.

----------


## Louck

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  ::): .

----------


## Blasteguaine

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)

----------


## Louck

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.

----------


## gbrinon

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  ::):

----------


## Louck

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  ::): .

----------


## Louck

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  :tired: . Ca réutilise beaucoup d'éléments que j'ai déjà conçu.

----------


## Hideo

Courage  ::P:

----------


## Louck

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.

----------


## Louck

*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  ::lol:: 


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  :;): .

----------


## Hideo

Cool de la lecture !  ::lol::  

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 ......

----------


## Louck

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  ::): .

----------


## am0

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 !

----------


## Louck

De rien  ::): .

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

----------


## Uriak

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  :;):

----------


## Louck

Je pense avoir compris  ::P: .

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 ?

----------


## Uriak

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  ::):

----------


## Louck

> 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.

----------


## Uriak

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.

----------


## Louck

> 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.

----------


## Louck

Soon.
(dans un autre topic  ::P: )

----------


## Louck

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  ::P: 

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é  ::P: ).


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 ?  ::P: 


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  ::P: .




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.

----------


## Raymonde



----------


## Uriak

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.

----------


## schouffy

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


Quand tu dis "nous sommes en 2015" c'est une façon de parler, ou ces techniques sont vraiment récentes ? Car ça parait vraiment trivial comme concept ?!

----------


## Orhin

> Quand tu dis "nous sommes en 2015" c'est une façon de parler, ou ces techniques sont vraiment récentes ? Car ça parait vraiment trivial comme concept ?!


C'est une façon de parler, ça existe depuis un paquet d'année.

----------


## Louck

Comme dit, c'est une façon de parler  ::P: .

Par contre il faut savoir que j'ai mis en place une interpolation toute bête pour ces démos.
En réalité, il faut réfléchir si notre jeu-vidéo est viable avec de l'interpolation ou de l'extrapolation, et il faut penser à comment bien le mettre en place. Car dans les faits, l'interpolation génère une certaine latence: l'information du serveur arrive un peu plus tardivement à notre écran.

Une mauvaise interpolation combiné à de la prédiction, tu as le fameux "peeking advantage" de CS:GO  :;): .

Mais cela n'a jamais été un problème très simple à régler. Tous les jeux ont ce genre de défaut ou quelque chose qui s'en approche.

----------


## xviniette

Le problème c'est que le tickrate signifie le nombre de "simulation" du monde par seconde et non le nombre d'envoie de snapshot/sec. Donc oui ça impacte les collisions, hittest, etc...

Pour plus d'informations : https://developer.valvesoftware.com/...sic_networking

Je pense qu'il est compliqué de critiquer le networking de CS:GO qui est un des (si ce n'est le) plus propres des jeux actuels.
Le peeking advantage est logique (même sans interpolation il y a peeking advantage), et je ne vois pas de façon de le supprimer.

----------


## Louck

Concernant la définition, de ce que je disais:




> 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.


Je suis d'accord que dans ta définition - où le serveur peut restreindre la fréquence de simulation de son monde - le tickrate peut avoir un certain impact sur le gameplay du jeu.

Si je l'ai précisé, c'est que la définition varie selon les moteurs utilisés, ou selon le contexte. Même si nous sommes d'accord que le wiki de Valve est une grosse référence quand on s'intéresse aux jeux en réseau, leurs définitions de tickrate/sendrate/updaterate sont propres à eux et à leur moteur de jeu.
Par exemple, dans l'ancienne version de l'API d'Unity3D, il est possible de paramétrer le "SendRate" du côté client et du côté serveur. Pour Valve, le "SendRate" n'est invoqué que du côté client. Ce n'est pas la même chose  ::): .

Bref, je ne veux pas jouer avec les mots non plus  ::): . Mais il est vrai qu'il est important de bien détailler sa propre définition des termes afin qu'il n'y ai pas de confusion. Certains joueurs possèdent ta définition du tickrate, d'autres ont la mienne (et bien d'autres ont leurs propres termes).





> Je pense qu'il est compliqué de critiquer le networking de CS:GO qui est un des (si ce n'est le) plus propres des jeux actuels.
> Le peeking advantage est logique (même sans interpolation il y a peeking advantage), et je ne vois pas de façon de le supprimer.


Sans aucun doute.

Il y avait eu une certaine mise à jour de CS:GO qui impactait un paramètre de l'interpolation (je n'ai plus le nom malheureusement). Hors à ce moment le paramètre était mal réglé, et le peeking advantage était obvious.
Mais je suis d'accord que tant que le jeu supporte la prediction, il y aura le peeking advantage. Etant donné que l'avantage est mesuré en dixième de secondes (sauf si mauvais paramétrage), je ne sais pas si cela une vrai importance dans les jeux compétitifs (EDIT: Sur les parties publiques, c'est "contré" par le lag compensation).


PS: Comment je vais bien me casser la tête sur mon jeu multijoueur pour bien configurer l'interpolation et la prédiction.

----------

