Networking & Multiplayer

Exista mai multe metode de a face un joc controlabil de mai multi jucatori in acelasi timp:

  • Non-network: jucatorii joaca pe acelasi dispozitiv prin ture, controland caractere diferite sau avand ecranul impartit
  • Network: jucatorii joaca pe dispozitive diferite, existand un server sau unul din juctori preluand si rolul de server (host) iar restul fiind clienti

Non-network

Pentru jocuri locale de multiplayer se foloseste in general o tehnica de split-screen: folosirea mai multor camere care se afiseaza impart viewport-ul intantei locale a jocului https://docs.unity3d.com/ScriptReference/Camera-rect.html.

Astfel, pentru un split-screen vertical se pot folosi doua camere cu valorile urmatoare pentru ViewPort Rect (x, y, width, height)

Camera 1 : (0, 0, 0.5, 1) ► left
Camera 2 : (0.5, 0, 0.5, 1) ► right

iar pentru pentru un split-screen orizontal

Camera 1 : (0, 0.5, 1, 0.5) ► top
Camera 2: (0, 0, 1, 0.5) ► bottom

Controlul jucatorilor va trebui sa se codeze separat prin definirea de axe sau taste specifice fiecarui jucator.

Daca se doreste implementarea unui GUI personalizat/diferit pentru fiecare jucator, se pot folosi masti de camera (culling masks). Se vor plasa toate obiecte Jucatorului 1 intr-un layer separat (de ex Player1Layer), si obiectele jucatorului 2 in alt layer (Player2Layer) si se va asigura ca pentru fiecare camera sunt selectate layer-urile corecte (se vor exclude pentru camera 1 layer-ul cu Player2, si pentru camera 2 layer-ul cu Player 1).

Network

Exista mai multe moduri de a programa un joc in retea. In general se foloseste un server separat programat special, la care se conecteaza clientii sub diverse forme. In functie de necesitati, se folosesc diverse metode de implementare a comunicarii, persistentei si a spatiului:

  • sesiuni de joc scurte pe spatii de dimensiuni limitate (de ex de tip meci) cu numar limitat de jucatori
  • sesiuni mari, persistente, fara limite de timp ce se desfasoara pe spatii mari si cu multi utilizatori conectati in acelasi timp (de ex de tip MMMO)

Pentru necesitati mici, un server dedicat nu este neaprat necesar, unul din clienti putand prelua si aceasta responsabilitate.

Unity pune la dispozitie un layer de networking de nivel inalt, HLAPI (Multiplayer High Level API), care permite realizarea de jocuri multiplayer relativ usor. HLAPI contine componente, scripturi si comenzi predefinite in pachetul UnityEngine.Networking, astfel incat sa usureze dezvoltarea multiplyer:

  • Message handlers
  • Serializare
  • Management distribuit pentru obiecte
  • Sincronizari
  • Elemente de networking de baza: Server, Client, Conexiune

Exista si servere dedicate stand-alone care se pot configura usor astfel incat sa fie integrate cu Unity:

Unity HLAPI Networking

Pentru a folosi functionalitatea implicita oferita de Unity in materie de networking, va trebui in primul rand sa initializat un script Network Manager, care se gaseste ca si componenta in meniu.

Acesta are nevoie de mai multe elemente de config, printre cele mai importante fiind:

  • adresa si portul serverului
  • referinta la Player - PlayerPrefab - aceasta referinta este de preferat sa nu fie deja in scena, intrucat se poate face spawn automat in anumite puncte, deci poate fi un prefab din library

Optiunea de Run in background, folosita pentru a putea rula mai multe instante pe aceeasi masina, poate creea probleme pe dispozitive mobile!

Pentru a adauga o referinta la un obiect in layerul de networking, acesta trebuie sa aiba cateva componenta importante:

  • Network Identity - astfel incat sa i se atribuie o identificare unica in server
  • Network Transform - astfel incat sa se propage transformarile in server

Pentru testare si prototipare usoara a partii de networking, se poate folosi componenta de NetworkManagerHud oferita de Unity.

Aceasta va afisa un meniu pentru conectare usoara.

Parcurgand pasii de mai sus va permite deja sa porniti un joc cu mai multi clienti si un server, care sa functioneze si sa sincronizeze miscarile jucatorilor.

Network Scripting

Scriptarea obiectelor intr-un joc de multiplayer are anumite particularitati pentru ca trebuie sa asigure mai multe elemente:

  • miscarea obiectelor trebuie sa fie sincronizata
  • anumite obiecte nu pot fi folosite decat de un client la un moment dat (inclusiv camera spre ex)
  • input-ul trebuie sa afecteze in general doar client-ul local (deci jucatorul curent nu si altii din retea)
  • anumite prelucrari trebuie efectuate doar pe server

Astfel, exista definite in pachetul UnityEngine.Networking, flag-urile urmatoare:

  • isLocalPlayer - true, daca este vorba despre obiectul de pe clientul local
  • hasAuthority - true, daca a fost instantiata sau preluata de obiectul de pe clientul local
  • isServer - true, daca scriptul ruleaza instanta curenta pe server

Astfel, in cazul unui script de PlayerController, trebuie sa ne asiguram ca actiunile sunt facute doar pentru clientul local. O metoda usoara, este prin stergerea efectiva a obiectelor/componentelor atunci cand acestea nu sunt locale. Sau, verific ca fiecare actiune sa fie facuta doar local.

using UnityEngine.Networking;
..

public class PlayerController : NetworkBehaviour {

  public GameObject player;
  
  void Start () {
  
    //varianta 1 - distrug componenta curenta pentru ceilalti, local, in clientul propriu
    if(!isLocalPlayer) {
      Destroy(this);
      return;
    }
  }
  
  void Update() {
  
    //sau verific ca afecteaza doar obiectul de pe clientul curent
    if(isLocalPlayer == true) {
        float translation = Input.GetAxis("Vertical") * Time.deltaTime;
        
        transform.Translate(0, 0, translation);
        
        float h = horizontalSpeed * Input.GetAxis("Mouse X");
        float v = verticalSpeed * Input.GetAxis("Mouse Y");

        transform.Rotate(v, h, 0);
        
        
        
        if(Input.GetAxis("Mouse ScrollWheel")) {
          //another gun - obiect creat cu autoritate pentru clientul local
          GameObject gun1 = (GameObject)Instantiate(Sniper, transform.position, Quaternion.identity);
          NetworkServer.SpawnWithClientAuthority(gun1, connection);
        }
        
    }
  }

}

Pentru ca obiectele sa poata fi instantiate din prefab-uri pentru networking, ele trebuie inregistrate in lista din setarile NewtorkManagerului:

Astfel, in cazul in care obiectele nu fac parte efectiv din player, cum ar fi cele de PickUp, sau arme care se pot schimba etc. putem folosi flag-ul de hasAuthority:

using UnityEngine.Networking;
..

public class Gun: NetworkBehaviour {
  
  void Update() {
    if(hasAuthority == true) {
    
        if(Input.GetAxis("Fire1")) {
          //fire bullet
        }
        
    }
  }
  
}

Un caz particular de obiecte instantiate sunt cele care ar putea crea coliziuni. Astfel, daca avem gloante instantiate ca obiecte, aceste trebuie sa:

  • se afiseze pe toti clientii
  • sa poata produce coliziuni pentru toti clientii

Pentru acest caz de obiecte avem la indemana posibilitatea de a trimite o comanda severului, astfel incat sa instantieze aceste tipuri de obiecte.

using UnityEngine.Networking;
..

public class Gun: NetworkBehaviour {
  
  void Update() {
    if(hasAuthority == true) {
    
        if(Input.GetAxis("Fire1")) {
          //fire bullet
          CmdFireBullet();
          
        }
        
    }
  }
  
  //Functie apelata de pe clientul local si rulata in server
  //Prefixul `Cmd` este necesar in fata denumirii functiei
  [Command]
  void CmdFireBullet() {
    //fire bullet - obiect creat pentru toti clientii
    GameObject bullet = (GameObject)Instantiate(Bullet, transform.position, Quaternion.identity);
    //add force , translation etc.
    
    NetworkServer.Spawn(bullet);
  }
  
}

Mai departe, pentru a scripta Bullet-ul in server, trebuie sa adaugam cateva elemente, astfel incat serverul sa gestioneze bucla de Update() si coliziunea efectiva.

public class Bullet : NetworkBehaviour {

  [ServerCallback]
  void Update() {
    life +=Time.deltaTime;
    if(life >= maxSpawnLife) {
      NetworkServer.Destroy(gameObject); //daca a trecut timpul maxim de existenta a unui bullet, il stergem din tot serverul (deci va disparea de la toti clientii)
    }
  }
  
  void OnCollisionEnter(Collision other) {
  
  //sectiune vizibila pentru toti clientii - animatii, audio, particule etc.
    hitParticles.Play(true);
    
    if(!isServer) //verific daca nu sunt pe server - si nu mai continui
      return; 
      
    //verificam mai departe coliziunile, doar o data, pe server
    PlayerController player = other.GameObject.GetComponent<PlayerController >();
    player.takeDamage(bulletDamage);
    
  }

}

Atentie! Toate obiecte pentru care vrem sa sincronizam transformarile trebuie sa aiba atasata componenta de NetworkTransform.

Inca un lucru important, ar fi elementul de camera, care ar trebui sa fie vizeze doar pentru clientul local. Astfel, in player controller putem avea ca referinta camera, pe care sa putem activa doar pentru clientul local.

public class PlayerController : NetworkBehaviour {

  public GameObject playerCamera;
  
  void Update() {
    if(isLocalPlayer == true) {
      playerCamera.SetActive(true);
    else 
      playerCamera.SetActive(false);
      
    ...
  }

Unity Network Lobby

DarkRift

DarkRift 2 este un server dedicat, modular, care implementeaza protocoale de comunicare ca TCP, UDP, dar si cu extensii pentru WebSocket sau RUDP. Pentru integrarea cu Unity, se poate folosi pachetul din Asset Store DarkRift Networking 2

Folosind un server dedicat trebuie gestionate mai multe elemente:

  • conexiunile si lista curenta de clienti conectati
  • datele stocate despre player
  • mesajele de update

DarkRift foloseste un sistem modular, bazat pe plugin-uri C#, pentru implementarea serverului. Astfel, putem defini un plugin in felul urmator:

public class NetPlugin : Plugin
{

    Dictionary<IClient, Player> players = new Dictionary<IClient, Player>();

    public override bool ThreadSafe => false;

    public override Version Version => new Version(1, 2, 5);

    public NetPlugin(PluginLoadData pluginLoadData) : base(pluginLoadData)
    {
        ClientManager.ClientConnected += ClientConnected;
        ClientManager.ClientDisconnected += ClientDisconnected;
    }
}

unde clasa Player reprezinta datele stocate despre player. Spre exemplu se pot stoca date legate de pozitie, rotatie sau id de utilizator.

public struct Player
{
    public ushort ID;
    public float X, Y, Z;
    public float rotX, rotY, rotZ;
}

In momentul in care se conecteaza un client, trebuie realizate urmatoarele lucruri:

  • instantierea Player-ului in server
  • notificarea tuturor celorlalti clienti conectati de conectarea unui nou player
  • trimiterea de informatii despre ceilalti clienti, clientului nou conectat
  • ascultarea de mesaje de actualizare
void ClientConnected(object sender, ClientConnectedEventArgs e)
    {
        //new player instance
        Player newPlayer = new Player();
        newPlayer.ID = e.Client.ID;
        newPlayer.X = 0;
        newPlayer.Y = 3;
        newPlayer.Z = 0;
        newPlayer.rotZ = 0;
        newPlayer.rotY = 0;
        newPlayer.rotZ = 0;
 
        using (DarkRiftWriter newPlayerWriter = DarkRiftWriter.Create())
        {
            newPlayerWriter.Write(newPlayer.ID);
            newPlayerWriter.Write(newPlayer.X);
            newPlayerWriter.Write(newPlayer.Y);
            newPlayerWriter.Write(newPlayer.Z);
            newPlayerWriter.Write(newPlayer.rotX);
            newPlayerWriter.Write(newPlayer.rotY);
            newPlayerWriter.Write(newPlayer.rotZ);
 
            //notify other clients
            using (Message newPlayerMessage = Message.Create(0, newPlayerWriter))
            {
                foreach (IClient client in ClientManager.GetAllClients().Where(x => x != e.Client))
                    client.SendMessage(newPlayerMessage, SendMode.Reliable);
            }
        }
 
        players.Add(e.Client, newPlayer);
 
        using (DarkRiftWriter playerWriter = DarkRiftWriter.Create())
        {
            foreach (Player player in players.Values)
            {
                playerWriter.Write(player.ID);
                playerWriter.Write(player.X);
                playerWriter.Write(player.Y);
                playerWriter.Write(player.Z);
                playerWriter.Write(player.rotX);
                playerWriter.Write(player.rotY);
                playerWriter.Write(player.rotZ);
            }
 
            //send all other players data to new client
            using (Message playerMessage = Message.Create(0, playerWriter))
                e.Client.SendMessage(playerMessage, SendMode.Reliable);
        }
 
        e.Client.MessageReceived += MovementMessageReceived;
    }

Mirror

..

Steam

..

UPlay

..

Photon Server

..

SmartFox

Cerinte

Aplicati o functionalitate de multiplayer (local sau network) pe unul dintre laboratoarele deja realizate.

pjv/laboratoare/2020/06.txt · Last modified: 2021/12/15 20:17 by alexandru.gradinaru
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0