5. Servere multiplayer dedicate

Cerinte

Realizarea unui joc multiplayer de tip social, prin folosirea/implementarea unui server multiplayer dedicat:

  • Player avatar sincronizat la nivel de pozitie si animatii: minim 2 playeri/clienti
  • Comunicare chat cu interfata grafica

Documentatie video

Documentatie text

Pentru acest laborator vom folosi DarkRift 2, 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

Server dedicat

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, astfel ca puteti programa mecanica serverului fie direct, fie bazat pe plugin-uri C#, pentru a nu recompila la fiecare modificare tot serverul.

Exemplu minimal:

using DarkRift;
using DarkRift.Server;

ServerSpawnData spawnData = ServerSpawnData.CreateFromXml("Server.config");

var server = new DarkRiftServer(spawnData);

void Client_MessageReceived(object? sender, MessageReceivedEventArgs e)
{
    using Message message = e.GetMessage();
    using DarkRiftReader reader = message.GetReader();
    Console.WriteLine("Received a message from the client: " + reader.ReadString());
}

void ClientManager_ClientConnected(object? sender, ClientConnectedEventArgs e)
{
    e.Client.MessageReceived += Client_MessageReceived;

    using DarkRiftWriter writer = DarkRiftWriter.Create();
    writer.Write("World of Hel!");

    using Message secretMessage = Message.Create(666, writer);
    e.Client.SendMessage(secretMessage, SendMode.Reliable);
}

server.ClientManager.ClientConnected += ClientManager_ClientConnected;

server.StartServer();

Console.ReadKey(); // Wait until key press. Not necessary in Unity.

cu un sistem de configurare in XML

<?xml version="1.0" encoding="utf-8" ?>
<!--
  Configuring DarkRift server to listen at ports TCP 4296 and UDP 4297.
-->
<configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://www.darkriftnetworking.com/DarkRift2/Schemas/2.3.1/Server.config.xsd">
  <server maxStrikes="5" />
  
  <pluginSearch/>
 
  <logging>
    <logWriters>
      <logWriter name="ConsoleWriter1" type="ConsoleWriter" levels="trace, info, warning, error, fatal">
        <settings useFastAnsiColoring="false" />
      </logWriter>
    </logWriters>
  </logging>

  <plugins loadByDefault="false"/>

  <data directory="Data/"/>

  <listeners>
    <listener name="DefaultNetworkListener" type="BichannelListener" address="0.0.0.0" port="4296">
      <settings noDelay="true" udpPort="4297" />
    </listener>
  </listeners>
</configuration>

Client

Pentru realizarea clientului puteti folosi direct pachetul din Asset Store DarkRift Networking 2

Exemplu de conectare si trimitere de mesaj simplu:

using DarkRift;
using DarkRift.Client;
using System.Net;

var client = new DarkRiftClient();

void Client_MessageReceived(object? sender, MessageReceivedEventArgs e)
{
    using Message message = e.GetMessage();
    using DarkRiftReader reader = message.GetReader();
    Console.WriteLine("Received a message from the server: " + reader.ReadString());
}

client.MessageReceived += Client_MessageReceived;

client.Connect(IPAddress.Loopback, tcpPort:4296, udpPort:4297, noDelay:true);

Console.WriteLine("Connected!");

using DarkRiftWriter writer = DarkRiftWriter.Create();
writer.Write("Hello world!");

using Message secretMessage = Message.Create(1337, writer);
client.SendMessage(secretMessage, SendMode.Reliable);

Console.ReadKey(); // Wait until key press. Not necessary in Unity.

Puteti folosi tag-uri pentru a delimita diverse tipuri de mesaje. De exemplu pentru actualizare de miscare puteti folosi tagul 1 si pentru chat puteti folosi tag-ul 10

Exemplu de trimitere mesaj de chat

    //This will be called when the user presses enter in the input field
    public void MessageEntered()
    {
        //Check we have a client to send from
        if (client == null)
        {
            Debug.LogError("No client assigned to Chat component!");
            return;
        }

        //First we need to build a DarkRiftWriter to put the data we want to send in, it'll default to Unicode 
        //encoding so we don't need to worry about that
        using (DarkRiftWriter writer = DarkRiftWriter.Create())
        {
            //We can then write the input text into it
            writer.Write(input.text);

            //Next we construct a message, in this case we can just use a default tag because there is nothing fancy
            //that needs to happen before we read the data.
            using (Message message = Message.Create(10, writer))
            {
                //Finally we send the message to everyone connected!
                client.SendMessage(message, SendMode.Reliable);
            }
        }
    }

Pentru actualizare miscarii, trebuie sa tinem cont de ficare client in parte:

  • trimtem actualizari de la input doar pentru clientul curent
void UpdateNetworkPosition() {
        if (
            Vector3.Distance(lastPosition, transform.position) > moveDistance ||
            Vector3.Distance(lastRotation, transform.eulerAngles) > moveDistance
            
            )
        {
            using (DarkRiftWriter writer = DarkRiftWriter.Create())
            {
                writer.Write(transform.position.x);
                writer.Write(transform.position.y);
                writer.Write(transform.position.z);
                writer.Write(transform.eulerAngles.x);
                writer.Write(transform.eulerAngles.y);
                writer.Write(transform.eulerAngles.z);

                using (Message message = Message.Create(1, writer))
                    Client.SendMessage(message, SendMode.Unreliable);
            }

            lastPosition = transform.position;
            lastRotation = transform.eulerAngles;
        }
    }
  • instantiem diferit playerii: unul controlabil din input local, pentru clientul curent, si ceilalti doar sincronizati la actualizarile din server
void SpawnPlayer(object sender, MessageReceivedEventArgs e)
    {

        using (Message message = e.GetMessage())
        using (DarkRiftReader reader = message.GetReader())
        {
            if (message.Tag == 0)
            {
                while (reader.Position < reader.Length)
                {
                    
                    ushort id = reader.ReadUInt16();
              

                    Vector3 position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                    Vector3 euler = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                    
                    GameObject obj;
 
                    if (id == client.ID)
                    {
                        // instantiate local user
                        obj = Instantiate(localPrefab, position, Quaternion.identity) as GameObject;
                     
                    }
                    else
                    {
                        // instantiate network user
                        obj = Instantiate(networkPrefab, position, Quaternion.identity) as GameObject;
                        
                        //keep a list with all network players
                        networkPlayerManager.Add(id, playObj);
                    }

                    
                }
            }
        }
    }
    
  • actualizam sincronizarile din server pentru toti playerii. Pentru asta in general avem nevoie sa tinem minte toata lista de clienti
Dictionary<ushort, NetworkPlayerObj> networkPlayers = new Dictionary<ushort, NetworkPlayerObj>();

void MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        using (Message message = e.GetMessage() as Message)
        {
            Debug.Log("network message ");
            if (message.Tag == 1)
            {
                // Debug.Log("position message ");
                using (DarkRiftReader reader = message.GetReader())
                {
                    ushort id = reader.ReadUInt16();
                    Vector3 newPosition = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                    Vector3 newEulerAngles = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());

                    if (networkPlayers.ContainsKey(id))
                    {
                        // Debug.Log("new network position: "+newPosition);
                        networkPlayers[id].movePosition = newPosition;
                        networkPlayers[id].currentEulerAngles = newEulerAngles;
                    }
                }
            }
        }
    }

Server Plugin

Putem folosi si un plugin modificand in configurare in felul urmator:

  <!--
    Specifies where DarkRift should look for plugins.
  -->
  <pluginSearch>
    <pluginSearchPath src="Plugins/" createDir="true" />
    <pluginSearchPath src="LogWriters/" />
    <pluginSearchPath src="NetworkListeners/" />
  </pluginSearch>
   <plugins loadByDefault="true">
    <!-- Example:
    <plugin type="Sniffer" load="false" />
    -->
  </plugins>

In acest caz veti face un proiect separat pentru plugin si il veti compila intr-un dll, care va fi pus/actualizat in folderul de Plugins.

using DarkRift;
using DarkRift.Server;
using System;
using System.Collections.Generic;

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;
    }

Actualizare de informatii

La fiecare actualizare (de ex miscarea personajului) trebuie facut broadcast.

void MovementMessageReceived(object sender, MessageReceivedEventArgs e)
    {
        using (Message message = e.GetMessage() as Message)
        {
            if (message.Tag == 1)
            {
                using (DarkRiftReader reader = message.GetReader())
                {
                    float newX = reader.ReadSingle();
                    float newY = reader.ReadSingle();
                    float newZ = reader.ReadSingle();
                    float newRotX = reader.ReadSingle();
                    float newRotY = reader.ReadSingle();
                    float newRotZ = reader.ReadSingle();

                    Player player = players[e.Client];

                    player.ID = players[e.Client].ID; //AA
                    player.X = newX;
                    player.Y = newY;
                    player.Z = newZ;
                    player.rotX = newRotX;
                    player.rotY = newRotY;
                    player.rotZ = newRotZ;

                    players[e.Client] = player; //AA

                    using (DarkRiftWriter writer = DarkRiftWriter.Create())
                    {
                        writer.Write(player.ID);
                        writer.Write(player.X);
                        writer.Write(player.Y);
                        writer.Write(player.Z);
                        writer.Write(player.rotX);
                        writer.Write(player.rotY);
                        writer.Write(player.rotZ);
                        message.Serialize(writer);
                    }

                    foreach (IClient c in ClientManager.GetAllClients().Where(x => x != e.Client))
                        c.SendMessage(message, e.SendMode);
                }
            }
        }
    }
   

Pentru sincronizarea de animatii se poate proceda in mai multe feluri, in functie si de modul de gestiune al animatiilor:

  • fie se trimite si un indicator de stare al animatiei si se sincornizeaza over network
  • fie se folosesc valori de speed, hieght etc (calculate prin diferenta sau luate/transmise direct)

Chat

Pentru a gestiona si mesajele chat, puteti adauga un event suplimentar in momentul in care se conecteaza clientul

e.Client.MessageReceived += ChatMessageReceived;

apoi sa ascultati mesajele si se face broadcast

    void ChatMessageReceived(object sender, MessageReceivedEventArgs e)
    {
        using (Message message = e.GetMessage() as Message)
        {
            if (message.Tag == 10)
            {
                using (DarkRiftReader reader = message.GetReader())
                {
                    using (DarkRiftWriter writer = DarkRiftWriter.Create())
                    {
                        writer.Write(reader.ReadString());
                    }

                    foreach (IClient c in ClientManager.GetAllClients())
                        c.SendMessage(message, e.SendMode);
                }
            }
        }
    }

Deconectarea

void ClientDisconnected(object sender, ClientDisconnectedEventArgs e)
    {
        players.Remove(e.Client);

        using (DarkRiftWriter writer = DarkRiftWriter.Create())
        {
            writer.Write(e.Client.ID);

            using (Message message = Message.Create(2, writer))
            {
                foreach (IClient client in ClientManager.GetAllClients())
                    client.SendMessage(message, SendMode.Reliable);
            }
        }
    }
    
 
pjv/laboratoare/2025/a05.txt · Last modified: 2025/12/10 12:46 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