samedi 10 novembre 2012

Tutorial: La mise en réseau sous Unity3D - Partie 2

Salut tout le monde.

Aujourd'hui on va continuer ce qu'on a commencé hier et on va vraiment chipoter à des composants réseau d'Unity.

Si vous avez loupé la première partie du guide c'est par-là que ça se passe.

Attention, hier je vous avais dit d’appeler un de nos scripts "StartNet" mais dans le script Cuby.js que je vous ait donné je l'ai utilisé en tant que "StartNetwork". Renommez le donc en StartNetwork. J'ai corrigé le billet d'hier donc si vous lisez cet article plus tard que le 9/11/2012 pas de problème^^.

Bien, on peut continuer.

Etablissement d'une connexion

  • Toujours dans votre scène mainMenu, créez un nouveau GameObject vide (Create Empty)
  • Appelez le "NetworkMaster" et assignez-lui le script StartNetwork. 
  • Faites glisser ce nouvel objet dans les Prefabs puis supprimez-le de la scène
  • Sélectionnez maintenant votre caméra. Dans l'Inspector vous verrez que son script MainMenu demande un GameObject NetworkMaster. Ça tombe bien, on vient d'en créer un ! Faites le glisser des Prefabs vers l'endroit prévu dans l'inspector.

Dans le script MainMenu, j'ai en fait précisé à Unity qu'il devait créer une nouvelle copie de NetworkMaster lorsqu'on appuie sur les boutons "Créer serveur" ou "Rejoindre serveur" du menu principal. Il configure ensuite ce Network Master selon qu'on soit serveur ou client. On a va maintenant configurer le script StartNetwork pour qu'il crée une connexion entre le serveur et les clients.
  • Ouvrez le script StartNetwork dans votre éditeur de code.
  • Supprimez la ligne #pragma strict (si vous codez en javascript). (Elle sert principalement quand on code sous iOS, xbox et ce genre de trucs et agit principalement comme un véritable générateur d'erreurs en mousse^^)
  • Avant la fonction Start() ajoutez une fonction Awake() et placez-y la ligne de code suivante: DontDestroyOnLoad(this);
Cela évitera que le script et son GameObject ne soient détruits lors du changement de scène.
  • Tout en haut du script, déclarez les variables suivantes
    • var server : boolean;
    • var listenPort : int = 25000; //le port d'écoute du serveur
    • var remoteIP : String; //l'adresse IP du serveur auquel les clients se connecteront
On a ensuite deux-trois trucs à changer dans notre script MainMenu.
Tout d'abord, j'ai fait une erreur dans la première version de la partie 1 du tutorial. Donc si vous l'avez suivi hier, un truc à changer:
  • Trouvez la condition suivante: if(GUI.Button(Rect(10, 20, sizeButtonX, sizeButtonY), "Créer serveur"))
  • Dedans j'ai écris la ligne scriptStartNet.server = false; 
  • Changez-la par scriptStartNet.server = true;
  • Ajoutez la ligne scriptStartNet.listenPort = serverPort; juste après
  • La condition if(GUI.Button(Rect(10, 60, sizeButtonX, sizeButtonY), "Rejoindre serveur")) est quant à elle correcte mais on va y ajouter scriptStartNet.remoteIP = serverIP; et scriptStartNet.listenPort = serverPort;
Votre script MainMenu doit maintenant ressembler à ceci:



Revenons à notre script StartNetwork et écrivons sa fonction Start().

Ce script gérant le serveur et les clients, on va commencer par une condition vérifiant si on est serveur ou client:

if(server)
{
Network.InitializeServer(32, listenPort, false); //le false signifie qu'on utilise pas le Nat punchtrough. Je vous recommande la doc d'Unity pour en savoir plus
// On préviens tous nos objets que le réseau est lancé
for (var go in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}

La première ligne met réellement en place le serveur. La connexion est ouverte aux clients. Vous aurez probablement besoin d'ajouter le Nat Punchtrough lorsque vous créerez un serveur sur internet, cette fonction va permettre de passer outre certains pare-feu afin de ne pas avoir besoin d'ouvrir les ports manuellement sur sa box/modem.

La boucle va envoyer un message à tous les GameObjects de la scène pour les avertir que le serveur est lancé.

Ensuite, ajoutez les lignes suivantes:
else
{
Network.Connect(remoteIP, listenPort);
}

Cela va connecter les clients au serveur.

A ce stade vous devriez être en mesure de builder le jeu, lancer la version buildée et cliquez sur "Créer serveur" puis lancer le jeu dans l'éditeur, cliquer sur "Rejoindre serveur" et tout devrais se passer sans erreurs. Vous ne remarquerez sans doute rien de spécial et pourtant vous venez d'établir votre première connexion Server-Client sous Unity, toutes mes félicitations :)

Comme je vous sens sceptiques vous pouvez aussi ajouter la fonction suivante:

function OnPlayerConnected(player: NetworkPlayer)
{
if(server)
{
print("Connecté !");
}
}

Buildez et lancer le jeu. Lancez-le aussi dans l'éditeur et cliquez sur "Créer serveur" dans l'éditeur puis sur "Rejoindre Serveur" dans la version buildée. Après un court instant  la console de l'éditeur devrait afficher "Connecté !". Il faut prendre le focus de la fenêtre contenant le serveur pour que le client réponde.

Cette fonction sera lancée à chaque fois qu'un client se connectera au serveur, elle peut notamment être utilisée pour garder le compte des joueurs connectés. La fonction inverse existe également: OnPlayerdisconnected(player:NetworkPlayer)

Je ne m'attarderai pas sur ce qu'est un NetworkPlayer parce que pour être honnête je ne maîtrise pas très bien ce concept. En gros c'est un peu ce qui relie un client et tous les objets qu'il instanciera sur le réseau.

Votre code doit désormais ressembler à ceci.


Lancer le niveau



Il ne nous reste plus qu'a lancer le level une fois la connexion établie.

On va commencer par ajouter la fonction suivante à notre script StartNetwork:

function OnLevelWasLoaded()
{
if ( Application.loadedLevelName == "mainMenu")
Destroy(this.gameObject);
// Notify our objects that the level and the network are ready
for (var go in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}

Cela va informer tous les objets de notre level au moment où il sera chargé qu'une connexion existe. On en profite également pour supprimer le NetworkMaster si on revient au menu principal afin d'éviter qu'il soit cloné à chaque fois.

Revenons à notre fonction Start(). 
  • A la fin de celle-ci, ajoutez Application.LoadLevel("Level");
Ceci lancera le level qu'on a créé dans la partie 1 (si vous lui avez donnez un autre nom que "Level", changez-le aussi dans le code). Le NetworkMaster doit normalement effectuer ce passage de level avec nous (comme précisé dans la fonction Awake() ).
  • Lancez le jeu pour voir si tout se passe comme prévu (n'oubliez pas d'ajouter le menu et le level dans la liste des niveaux à builder dans File/Build Settings, le menu en première position)
Vous ne devriez pas avoir d'erreurs et être capable de vous connecter au serveur.

Instancier les joueurs sur le réseau



On va maintenant créer un Cuby (notre cube d’assaut) par joueur (serveur y compris).

Dans la scène "Level", ajoutez un nouvel Empty GameObject. Appellez le "Spawn" et positionnez-le au-dessus du sol (pas trop haut mais pas trop bas non plus pour que Cuby ne spawn pas dans le sol). Taggez le "Spawn" et créez en plusieurs un peu partout, on fera apparaitre Cuby sur un Spawn aléatoire.

De retour dans notre script StartNetwork() on va ajouter quelques lignes à la fin de la fonction OnLevelWasLoaded()
  • Ajoutez
    • spawners = GameObject.FindGameObjectsWithTag("Spawn");
    • var rand : int = Random.Range(0, spawners.length);
    • var spawn : GameObject = spawners[rand];
Cela aura pour effet de regarder tous les spawn du niveau et d'en choisir un aléatoirement.

On va ensuite ajouter une commande dont vous aurez souvent besoin lorsque vous coderez des jeux réseaux:
  • cubyInst = Network.Instantiate(cuby, spawn.transform.position, Quaternion.identity, 0);
Elle fonctionne globalement comme Instantiate sauf qu'il y a une option en plus. Cette ligne de code va instancier le GameObject "cuby" en réseau à la position de spawn (le Spawn choisi aléatoirement) , orienté normalement (les rotations seront les mêmes que celles du préfab Player).

Quelle est la différence avec la fonction Instantiate normale alors?, me direz-vous. En fait l’instanciation va se passer sur tous les clients connectés et sur le serveur et pas seulement sur la machine exécutant le jeu. Ça signifie que votre Cuby sera créé chez tout le monde. Avec une instanciation normale, seul vous seriez capable de voir votre perso. Peu pratique pour faire des headshots aux ennemis^^.

Le dernier argument désigne le groupe de clients chez qui on instancie notre Player/Cuby. On va le laisser à 0 pour qu'il spawn chez tout le monde.

Il ne reste plus qu'à déclarer les variables qu'on vient d'utiliser. 
  • Au début du script, déclarez:
    • private var spawners : GameObject[];
    • var cuby : GameObject;
    • var cubyInst : GameObject;
Retournez sur Unity et assigner notre Prefab Player à la variable cuby en le faisant glisser dans l'inspector.


Si vous lancez votre jeu (depuis le menu principal) et que vous créez un serveur, votre Cuby devrais spawner et arriver.. de travers... 
  • Maudissez les quaternions auquel je n'ai jamais rien compris puisqu'une rotation n'est pas censée être découpée en 4 valeurs, c'est contre nature...
  • Ajoutez cubyInst.transform.eulerAngles = Vector3(0, 0, 90); après cubyInst = Network.Instantiate(cuby, spawn.transform.position, Quaternion.identity, 0);
On va également ajouter un petit délai au client avant qu'il n'instancie son Cuby pour lui laisser le temps de se connecter au serveur.

Ajoutez:
if(!server)
yield WaitForSeconds(3);
avant cubyInst = Network.Instantiate(cuby, spawn.transform.position, Quaternion.identity, 0);

Relancez le jeu et ça devrais fonctionner.
Remarque:
Quand vous lancez client et serveur sur une seule machine vous remarquerez que rien ne se passe côté client si vous ne cliquez pas de temps en temps sur la fenêtre serveur. Cela est dû au fait qu'Unity met le jeu en pause quand vous perdez le focus de la fenêtre.
  • Pour régler ça, dans Unity, cliquez sur Edit/Project Settings/Player
  • Dépliez le panneau "Settings for PC and Mac Standalone"
  • Cochez finalement la case "Run in Background"
Votre code doit ressembler à ça


Vous pouvez maintenant lancer le jeu en tant que serveur et client (lancez le deux fois, créer un serveur et connectez-vous sur 127.0.0.1 avec le client

Au moment où vous lancerez votre jeu, vous devriez normalement vous dire un truc du genre:

Oh mais ça marche !
...Hey mais... ! Attends une minute... Beldir mais tu n'es qu'un sale charlatan !! Le joueur adverse est de travers et en plus il avance en même temps que moi et je ne vois pas ses déplacements à lui !!

Pas de panique, tout cela est tout à fait normal à ce stade.

Séparer le fonctionnement de nos objets et celui de ceux de l'adversaire



Nous avons trois problèmes

  • Ce n'est pas vous qui avez instancié le joueur adverse, et vous ne lui avez donc pas dit de se tourner correctement. 
  • De plus le joueur adverse est exactement comme le votre. C'est à dire qu'il possède un script Cuby.js qui réagit donc à vos frappes de touches.
  • On ne voit pas les déplacements que l'adversaire effectue de son côté
Nous allons tout de suite remédier au troisième problème et vous verrez que le premier se réglera en même temps. On verra ensuite comment empêcher le déplacement de l'ennemi quand on appuie sur nos touches à nous.
  • Dans Unity, sélectionnez votre Prefab Player et ajoutez lui une NetworkView (Component/Miscellaneous/Network View)
Problème réglé ! La NetworkView va synchroniser la position et l'orientation de notre Cuby local et de notre Cuby tel qu'il apparaît sur l'écran des adversaires. N'hésitez pas à consulter la doc d'Unity pour en apprendre plus sur les NetworkView.

Mais l'ennemi se déplace toujours en même temps que nous et la console spamme plein d'avertissements.
Le spam est dû au fait qu'on ait deux caméras activées avec un AudioListener dessus dans notre scène. En plus de spammer la console cette seconde caméra consomme aussi beaucoup de mémoire.

On va régler ça tout de suite.
  • Ouvrez le script Cuby et ajoutez les lignes suivantes dans la fonction Start():
if ( !networkView.isMine) //si ce perso ne m'appartient pas
{
Destroy(cameraPlayer);
this.enabled = false;
}

Cela va dire à Unity de désactiver ce script si la NetworkView associée à l'objet le contenant n'est pas à nous. Autrement dit, si l'objet n'a pas été instancié par nous sur le réseau.

On en profite pour désactiver la caméra attachée aux Cuby ne nous appartenant pas.

  • Déclarez la variable var cameraPlayer : GameObject; dans le script et associez-y la caméra attachée à Cuby dans les Préfabs par glisser/déposer. 


Je vous met le code qu'on vient de taper en même temps (ne faites pas attention à la variable "life" que j'ai finalement laissée tomber dans la partie 3^^).

Conclusion



Voila, c'est tout pour cette partie 2. Vous devriez maintenant être capable de vous promener dans le niveau et de connecter autant de joueurs que vous voulez. Votre jeu doit pouvoir fonctionner en réseau local mais aussi sur internet. S'il ne fonctionne pas sur internet, pensez à essayer d'activer le Nat punchtrough ou utiliser hamachi ;) (ou Tunngle qui est mieux, héhé).

Retenez bien ce "networkView.isMine". Vous l'utiliserez extrêmement souvent. D'abord pour, comme ici, choisir quels scripts doivent être ou ne pas être activés sur un objet instancié sur le réseau, mais aussi pour plein d'autres raisons dont aucune ne me vient à l'esprit pour l'instant.

Lorsque vous créez un script, demandez-vous toujours s'il sera oui ou non activé pour tout le monde ou seulement par celui qui l'instancie. Par exemple j'ai mis la vie de Cuby dans le script Cuby.js qu'on vient de désactiver pour tous les autres joueurs... Je sais ce que je fais et c'est pour vous montrer quelque chose plus tard mais il aurait peut-être été plus facile en conditions normales de créer un script InfoPlayer contenant ce genre d'informations d'états du joueur.

Voila voila.

Dans la prochaine partie du tutorial on verra comment gérer les tirs de nos Cuby (vous devriez déjà avoir une idée) et on rendra le jeu plus ou moins jouable.

Si ça ne fonctionne pas chez vous, je vous recommande de relire tout mais si vraiment vous ne vous en sortez pas voila mon projet Unity, là où nous en sommes arrivés:

Télécharger le fichier.

A tout de suite dans la partie 3 !