Comment créer un jeu multijoueur rapidement avec PUN (Photon unity Networking) ?

Photon Unity ou PUN (Photon unity Networking) est un moteur réseau qui permet, en très peu de lignes, de faire ce que vous voulez.

Avant d’écrire l’article théorique sur le multijoueur, j’ai testé plusieurs moteurs :

  • Un serveur en NodeJs discutant en socketIO les clients Unity.
  • UN ou UNet pour unity network, qui est le moteur réseau proposé directement par Unity.
  • PUN qui me semble le plus intéréssant, au moins pour commencer et comprendre comment la couche réseau fonctionne.

J’en ai vu d’autres, mais que je n’ai pas pris le temps de tester :

Pourquoi PUN ?

  • Déjà parce qu’il est gratuit.
  • Il permet de ne pas avoir à maintenir un serveur, ni à écrire le code allant avec.
  • Il propose un cloud gratuit pour 20 personnes pour transmettre les données d’un client à l’autre.
  • Il est simple et efficace. En ajoutant une classe à un objet, on a presque déjà un jeu multi !
  • On peut aller assez loin dans l’optimisation et qu’on peut redéfinir les méthodes à notre sauce.

La version payante ajoute plus de places pour les joueurs et une gestion de file d’attente pour rejoindre s’il y a trop de monde.

Créer un compte

De la même manière que dans l’article sur Firebase, on va utiliser un service extérieur à Unity. Dans ce cas, on va utiliser le cloud de PUN pour aider à transmettre les informations entre les joueurs.

Pour ce faire, il faut créer un compte sur le site de photon https://www.photonengine.com/en-US/Account/SignUp.

Vous devriez recevoir un mail avec le mot de passe que vous pourrez personnaliser.

Une fois connecté, vous allez pouvoir créer un projet.

Vous pouvez créer d’autres projets pouvant gérer 20 CCU ou ConCurrent Users. Ce qui signifie que vous pouvez gérer un jeu de 20 utilisateurs simultanés. Que ce soit via une session de 20 joueurs ou créer 5 sessions en même temps de 4 joueurs. On reviendra là-dessus. Ce qui va nous intéresser par la suite, c’est le App Id, en cliquant dessus, l’id complet va apparaître pour pouvoir le copier.

C’est tout ! Aucun réglage compliqué, le plus gros sera géré par votre projet Unity.

 

Import d’un projet

Ayant besoin d’une scène et d’au moins une entité à contrôler pour interagir en réseau, vous pouvez directement utiliser votre projet et appliquer ce qu’on va voir ensemble. Si ce n’est pas le cas, je vous propose de partir via la scène que j’avais faite pour montrer une caméra TPS, que vous pouvez télécharger par ici :

Ajouter PUN à Unity

Je vous laisse créer un projet ou utiliser un projet existant. Je vais vous fournir les assets avec une scène toute prête et vous présenter les différents éléments.

Une fois que c’est prêt, on va commencer par importer Photon Unity Networking Free. Il existe plusieurs versions donc vérifiez bien que c’est bien celle-ci.

Une fois importé, vous devriez avoir une fenêtre de configuration qui va s’ouvrir. Il faudra y renseigner l’App Id provenant de votre dashboard de PUN.

Si vous l’avez raté, pas de soucis, vous pouvez le rouvrir en passant dans « Window => Pun Wizard => Setup Project« .

 

Dans les deux cas, vous devriez avoir un fichier qui s’est créé dans « Assets/Photon Unity Networking/Resources« . Ce dossier est important car il devra contenir l’ensemble des prefabs que vous allez propager par le réseau, mais on y reviendra.

Ce fichier de configuration peut être modifié, vous retrouverez votre App Id, et quelques options comme le cloud à utiliser (Ici EU ou Europe). Je vous conseille d’utiliser l’option « Auto-Join Lobby » qui pour un prototype vous permettra de ne pas avoir à gérer plusieurs écrans avant de se connecter.

Avant de passer à la suite, revenons sur le dossier « Assets/Photon Unity Networking/Resources« . Au lieu d’instancier des prefabs comme on le ferait normalement, PUN propose des méthodes qui permettent d’instancier sur tous les clients connectés. Cependant, pour qu’il puisse avoir accès à ces prefabs, il oblige à ajouter tous les prefabs dans son dossier.

Dans l’exemple qui va suivre, je n’ai ajouté qu’un seul prefab mais vous aurez vite l’obligation de tous les référencer dans ce dossier.

 

Utilisation de PUN

Maintenant que vous avez PUN sur votre projet, on va pouvoir voir comment l’utiliser.

 

Pour simplifier la suite, je vous propose d’ajouter un SpawnPoint afin d’avoir une position pour que les joueurs puissent apparaitre. Placez à votre convenance, légèrement au-dessus du sol ou plus haut.

 

Se connecter à PUN

Afin de se connecter du NetworkManager. On va créer un script qui va gérer ça pour nous, j’ai essayé d’expliquer au maximum dans les commentaires, mais n’hésitez pas à me dire si vous voulez plus d’explications.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class NetworkManager : MonoBehaviour {
    public GameObject PlayerPrefab, SpawnPoint;
    
    void Start () {
        // On se connect directement au cloud de PUN, si vous avez sélectionné auto join dans la configuration
        // On devrait donc se connecter directement au Lobby
        PhotonNetwork.ConnectUsingSettings("tutopun");
    }

    // Après le Start() on rejoint le Lobby, c'est à dire qu'on est connecté au cloud de PUN
    // Maintenant il faut rejoindre une Room
    void OnJoinedLobby()
    {
        // On va créer une room pour 4 personnes maximun
        RoomOptions MyRoomOptions = new RoomOptions();
        MyRoomOptions.MaxPlayers = 4;

        // Ici on set de manière aléatoire le nom de l'utilisateur pour aller plus vite
        PhotonNetwork.playerName = "Player" + Random.Range(1, 500);

        // Enfin on rejoint la room demandé, du nom de "The Game" mais on pourrait mettre un nom différent
        // afin de créer une autre room
        PhotonNetwork.JoinOrCreateRoom("The Game", MyRoomOptions, TypedLobby.Default);
    }

    // Quand on a effectivement rejoint la room
    void OnJoinedRoom()
    {
        // On instancie le joueur à tout le réseau
        // Nom du prefab à instancier / Position / Rotation / Groupe
        // Le groupe permet de différencier des joueurs, ou d'autre éléments, que vous vouliez différencier rapidement
        // les joueurs des adversaires OU différencier les équipes des joueurs
        PhotonNetwork.Instantiate(PlayerPrefab.name, SpawnPoint.transform.position, Quaternion.identity, 0);
    }
}

 

Pour que PUN puisse utiliser notre personnage, il faut l’ajouter dans le dossier Ajout sur le personnage Player,

Le fait que notre personnage est dans le dossier de photon n’est pas suffisant. Il faut aussi ajouter un composant Photon View à ce prefab. Il contient une liste d’objets observés qui seront transmis par réseau à tout le monde. En déplaçant le transform de l’objet dans cette propriété, on va donc envoyer à chaque fois que change la position ou la rotation, la mise à jour à tout l’ensemble des personnes connectées.

 

Dans le composant Photon View, on peut remarquer quelques propriétés.

  • Owner, Uniquement modifiable pendant que le projet tourne. Chaque game object est géré par un client et seulement un, le owner est celui qui s’occupe de mettre à jour (et transmettre sur le réseau) l’ensemble des informations le concernant.
    • Fixed : Le propriétaire reste celui qui l’a instancié. En mode client/serveur, un client gérant les éléments ce sera à ce moment lui qui instanciera les éléments nécessaire et les gérera.
    • Takeover : N’importe quel client peut prendre le contrôle du gameobject quand il le souhaite. Peut être utile par exemple si chaque joueur gère les objets autour de lui pour ne pas charger trop d’éléments.
    • Request : De la même manière que le mode Takeover, n’importe quel client peut prendre le contrôle mais ajoute une confirmation de la part du propriétaire actuel.
  • View Id, qui se doit d’être unique, mais rassurez vous, quand vous l’ajoutez à un élément, il s’incrémente. Donc dans le cas normal, vous n’aurez pas à vous en soucier. Il permet lors des communications réseau de savoir de quel élément on parle, d’où l’importance d’être unique. D’ailleurs, si vous éditez un prefab hors de la scène, la valeur sera « Set at runtime ». L’id sera déterminé automatiquement lors de l’instanciation.
  • Observer option, permet de déterminer la fréquence d’actualisation sur le réseau.
    • Off : Rien ne sera transmis, si vous voulez passer hors ligne par exemple
    • Reliable Delta Compressed : Lors de l’appel à la méthode Update() fournit par Unity, s’il y a un changement PUN va compresser les données puis transmettre les données sur le réseau et vérifier à chaque fois que chaque client a bien reçu la mise à jour, sinon il la renverra. Utile quand on pas besoin d’être rapide mais d’être particulièrement précis puisque le décalage pour transmettre l’information peut prendre plus de temps : par exemple, un jeu tour par tour où vous voulez être sûr d’avoir les bonnes données.
    • Unreliable : Envoyé sur le réseau mais sans vérifier que la mise à jour à bien a bien été effectuée. Pour ceux connaissant le réseau, c’est exactement le comportement du protocole UDP. Très utile pour des mises à jours très fréquentes comme le déplacement d’un joueur. Dans le cas d’un FPS où on a besoin d’être rapide, on aura de la perte d’information mais dans la masse, on ne s’en rendra pas compte.
    • Unreliable On Change : Lors de l’appel à la méthode Update() fournit par Unity, s’il y a un changement on arrivera dans le cas du Unreliable qui permet de diminuer le trafic réseau tant que ce n’est pas utile. Je ne vois pas de cas très utile… puisqu’on va chercher à envoyer les informations rapidement, mais mettre à jour uniquement sur un changement… donc si on a raté le changement et qu’il n’y en a pas, on aura donc une désynchro qui peut durer un moment. Ce cas est donc pratique si vos variables évoluent très souvent, par exemple la position d’un joueur en FPS qui de toute façon bougera très souvent, donc si on perd quelques données, la position ne sera que légèrement erronée.
  • Transform Serialization, n’apparait que si on veut observer un transform comme c’est notre cas ici pour déterminer ce qui doit être mis à jour.

 

Pour tester un jeu multijoueur, on est obligé de générer le jeu CTRL+SHIFT+B par défaut ou « File/Build Settings ».

Pensez à ajouter la scène et lancer le build.

Une fois que ce sera fait vous pourrez lancer le jeu (de préférence en fenêtré) et dans Unity. Vous devriez avoir quelque chose comme ça…

Quand vous bougez, tout le monde bouge, et en plus il y a 3 personnages pour 2 joueurs connectés… Bref ce n’est pas terrible, mais on a déjà un jeu en multijoueur !

 

Corrections du personnage supplémentaire

Le personnage supplémentaire a été laissé pour montrer une erreur classique de laisser le prefab dans la scène, il suffit simplement de le supprimer. Mais avant de le supprimer, profitons-en pour désactiver la caméra ainsi que l’audio listener, sinon tous les joueurs utiliseront la caméra du dernier connecté.

De la même manière, on va empêcher le contrôle de l’ensemble des personnages en désactivant le TPSController script sur le Player.

Surtout avant de supprimer le Player de la scène, n’oubliez pas d’appliquer les changements au prefab. Si vous avez modifié les éléments depuis le prefab dans la scène bien sûr !

Si tout est bon, dans ce cas, il ne reste qu’à supprimer le player. On pourrait uniquement désactiver le player dans la scène qui permet de le réactiver pour faire des tests. Cependant, je ne le recommande pas, le fait de le désactiver, vous oblige à le réactiver chaque fois que vous le modifier et que vous voulez enregistrer via le bouton « Apply ». Pour avoir déjà essayé, c’est une source potentielle d’erreur assez importante.

Maintenant il nous faut activer la caméra, l’audio listener et le script de contrôle. Pour ce faire, on va les activer uniquement pour le joueur qui se connecte. La commande PhotonNetwork.Instantiate() permet de demander à tout le monde en réseau de créer le prefab. Mais le reste de la méthode reste exécuté uniquement en local.

    // Quand on a effectivement rejoint la room
    void OnJoinedRoom()
    {
        // On instancie le joueur à tout le réseau
        // Nom du prefab à instancier / Position / Rotation / Groupe
        // Le groupe permet de différencier des joueurs, ou d'autre éléments, que vous vouliez différencier rapidement
        // les joueurs des adversaires OU différencier les équipes des joueurs
        GameObject MyPlayer = PhotonNetwork.Instantiate(PlayerPrefab.name, SpawnPoint.transform.position, Quaternion.identity, 0);

        // Le contrôle et la caméra sont désactivé afin d'être activé UNIQUEMENT pour le joueur en local
        // Et surtout pas contrôlé par les autres joueurs sur le réseau
        MyPlayer.GetComponent<TPSController>().enabled = true;
        MyPlayer.GetComponentInChildren<Camera>().enabled = true;
        MyPlayer.GetComponentInChildren<Camera>().GetComponent<AudioListener>().enabled=true;
    }

Si vous testez, vous ne devriez plus avoir les problèmes qu’on vient de régler. Il reste cependant un problème, l’animation du personnage ne fonctionne qu’en local mais pas sur les autres clients !

 

Propagation de l’animator sur le réseau

On propage sur le réseau les mouvements du personnage, mais on n’a rien fait concernant les animations. Allons du côté de l’animator, qui se trouve dans notre exemple dans le premier sous élément du player. Mais peut se trouver sur l’objet racine sur votre projet.

Dans les configurations, ajoutez un composant Photon Animator View s’il n’était pas présent, il ajoutera automatiquement le composant Photon View. Il nous reste à ajouter ce photon animator view dans les objets observés du photon view pour être propagé sur le réseau. Contrairement au transform, l’animator ne peut pas être envoyé facilement. Une position qui évolue dans le temps c’est facile, on peut ignorer des données et continuer en corrigeant. Mais l’animator lance des animations via des événements, des triggers, … il est assez compliqué d’envoyer par réseau où on en est rendu dans l’animation et laquelle ? Le composant de photon s’occupe d’envoyer les événements pour lancer les bonnes animations.

Vérifiez que vous propagez le layer et le state en ayant l’option Discrete comme sur le screenshot, sinon vous risquez de ne pas envoyez les informations suffisantes pour effectuer des animations.

Concernant les propriétés de synchronisation, vous avez la liste des couches et de variables de votre animator. Il existe plusieurs configurations à utiliser selon vos besoins.

  • Discrete : Met à jour à chaque appel de OnPhotonSerializeView() à savoir 10 fois par seconde. Conseillé dans les cas simples.
  • Continuous : Envoie toutes les valeurs intermédiaires, c’est à dire la valeur actuel, mais aussi toutes les valeurs depuis la dernière mise à jour. Fonctionne aussi avec la méthode OnPhotonSerializeView() à savoir 10 fois par seconde. Ce qui permet une animation plus fluide mais un trafic réseau plus lourd

 

La suite ?

Gérer les cas de déconnexions

Pour la suite, il faudrait synchroniser les différentes boules sur le projet pour que tout le monde voit la même chose lorsqu’on les pousse. Si vous ne voulez pas avoir besoin d’aller plus loin que ce tuto, vous pouvez les instancier quand le premier client se connecte en premier en utilisant la méthode OnCreatedRoom () dans le NetworkManager. Enfin d’activer la gestion uniquement pour celui qui l’a instancié comme on a fait pour le joueur. Ainsi seul le premier joueur gérera les boules et propagera leur mouvement.

L’autre solution qui permettrait de gérer le cas de client se déconnectant, étant de laisser actif les scripts sur les game object, et d’ajouter cette condition : if(photonView.isMine) qui reviendra pour les cas simple à la même chose qu’on vient de faire.

Rendre plus fluide les mouvements

En testant vous vous en êtes peut être rendu compte, mais les déplacements se font bien sur le réseau mais pas encore de manière très fluide, les personnages ont tendance à se téléporter sur de courtes distances. Le but étant de gérer la mise à jour via quelques méthodes mathématique comme Vector3.Lerp qui permet d’utiliser la position actuelle, et la position qu’on doit atteindre pour la corriger sur un temps donné. Au lieu de passer de la position A à B d’un coup, on va passer par toutes les positions intermédiaires durant un court laps de temps. Un décalage par rapport à la position réelle va apparaître, mais on pourra corriger ça avec de la prédiction.

La prédiction consiste à savoir que le joueur court dans tel direction, et supposer qu’il va le faire pendant encore quelques frames. Si je reçois l’ordre par le réseau qu’il a bougé, sachant que j’ai déjà un décalage, je peux supposer que sur le pc donnant l’ordre, il est déjà un peu plus loin dans cet même direction.

 

 

Si vous avez des problèmes

 

Prefab réseau

Vous pouvez utiliser vos prefabs hors du dossier de PUN, en créant un dossier « Resources« .

Souvent je vais créer un sous dossier prefabs et je pourrais instancier via :

PhotonNetwork.instantiate("Prefabs/" + monPrefabEnVariable.name, Vector3.zero, Quaternion.Identity);

 

Mauvais prefab

Si vous avez une erreur comme :

Failed to Instantiate prefab: PlayerCustoa. Verify the Prefab is in a Resources folder (and not in a subfolder)

Ou un prefab qui n’a rien n’avoir avec le votre par exemple (dans le cas d’un prefab « player »)

Dans ce cas, vérifiez bien que votre prefab est bien placé dans les dossiers Resources/ et au même niveau que l’endroit d’où vous l’appelez (voir le point précédent).

 

source : https://doc.photonengine.com/en-us/pun/current/getting-started/pun-intro

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *