This shows you the differences between two versions of the page.
|
pjv:laboratoare:2025:a05 [2025/09/29 08:35] alexandru.gradinaru |
pjv:laboratoare:2025:a05 [2025/12/10 12:46] (current) alexandru.gradinaru [Cerinte] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ===== 5. Servere multiplayer dedicate ===== | ===== 5. Servere multiplayer dedicate ===== | ||
| + | |||
| + | <hidden> | ||
| + | |||
| + | laborator bonus | ||
| + | |||
| + | de explicat sumar: | ||
| + | * - ce e darkrift si unde se gasesc resurse | ||
| + | |||
| + | github [[https://github.com/DarkRiftNetworking/DarkRift ]] | ||
| + | si plugin pt client [[https://assetstore.unity.com/packages/tools/network/darkrift-networking-2-95309]] | ||
| + | |||
| + | |||
| + | [10.12.2025, 11:26:43] Alex Gradinaru: hmm, ma mai gasit aici un tutorial, care foloseste si playfab pentru lobby | ||
| + | [10.12.2025, 11:26:43] Alex Gradinaru: https://dev.to/robodoig/unity-multiplayer-bottom-to-top-46cj | ||
| + | [10.12.2025, 11:28:39] Alex Gradinaru: pare interesant, dar probabil pt iteratia urmatoare de lab 🙂 | ||
| + | [10.12.2025, 11:28:55] Alex Gradinaru: vad ca e si hosted on playfab | ||
| + | [10.12.2025, 11:29:50] Alex Gradinaru: si inca un tutorial interesant, cu predictii pt FPS https://lukestampfli.github.io/EmbeddedFPSExample/guide/introduction.html | ||
| + | [10.12.2025, 11:30:04] Andrei Lapusteanu: https://lukestampfli.github.io/EmbeddedFPSExample/guide/networking-discussion.html#networking-architectures | ||
| + | [10.12.2025, 11:30:15] Andrei Lapusteanu: Haha de pe acelasi tutorial :) | ||
| + | [10.12.2025, 11:31:42] Alex Gradinaru: nu mai e accesibila documentatia, dar au mai ramas niste links utile prin github | ||
| + | https://github.com/DarkRiftNetworking/DarkRift/wiki/%23-Useful-Links | ||
| + | [10.12.2025, 11:31:51] Alex Gradinaru: nu gasesc inca legat de chat | ||
| + | [10.12.2025, 11:32:44] Alex Gradinaru: ah, e tot pe git | ||
| + | https://github.com/DarkRiftNetworking/DarkRift.Documentation | ||
| + | [10.12.2025, 11:34:07] Alex Gradinaru: mda, tot nu are ceva legat de chat, dar cred ca foloseam doar sistemul de message si broadcast simplu | ||
| + | |||
| + | alte idei : https://chatgpt.com/share/6935b217-e87c-8004-9a38-d216be4d634c | ||
| + | |||
| + | </hidden> | ||
| + | |||
| + | ==== 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 [[https://github.com/DarkRiftNetworking/DarkRift|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 [[https://assetstore.unity.com/packages/tools/network/darkrift-networking-2-95309|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: | ||
| + | |||
| + | <code> | ||
| + | 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. | ||
| + | |||
| + | </code> | ||
| + | |||
| + | cu un sistem de configurare in XML | ||
| + | |||
| + | <code> | ||
| + | <?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> | ||
| + | |||
| + | </code> | ||
| + | |||
| + | ==== Client ===== | ||
| + | |||
| + | Pentru realizarea clientului puteti folosi direct pachetul din Asset Store [[https://assetstore.unity.com/packages/tools/network/darkrift-networking-2-95309|DarkRift Networking 2]] | ||
| + | |||
| + | Exemplu de conectare si trimitere de mesaj simplu: | ||
| + | <code> | ||
| + | |||
| + | 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. | ||
| + | |||
| + | </code> | ||
| + | |||
| + | 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 | ||
| + | <code> | ||
| + | //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); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | Pentru actualizare miscarii, trebuie sa tinem cont de ficare client in parte: | ||
| + | * trimtem actualizari de la input doar pentru clientul curent | ||
| + | <code> | ||
| + | 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; | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | * instantiem diferit playerii: unul controlabil din input local, pentru clientul curent, si ceilalti doar sincronizati la actualizarile din server | ||
| + | <code> | ||
| + | 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); | ||
| + | } | ||
| + | |||
| + | | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | </code> | ||
| + | * actualizam sincronizarile din server pentru toti playerii. Pentru asta in general avem nevoie sa tinem minte toata lista de clienti | ||
| + | <code> | ||
| + | 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; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | ==== Server Plugin ===== | ||
| + | |||
| + | Putem folosi si un plugin modificand in configurare in felul urmator: | ||
| + | |||
| + | <code> | ||
| + | <!-- | ||
| + | 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> | ||
| + | </code> | ||
| + | |||
| + | 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. | ||
| + | |||
| + | <code> | ||
| + | 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; | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | unde clasa Player reprezinta datele stocate despre player. Spre exemplu se pot stoca date legate de pozitie, rotatie sau id de utilizator. | ||
| + | <code> | ||
| + | public struct Player | ||
| + | { | ||
| + | public ushort ID; | ||
| + | public float X, Y, Z; | ||
| + | public float rotX, rotY, rotZ; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | 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 | ||
| + | |||
| + | <code c#> | ||
| + | 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; | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | ==== Actualizare de informatii ===== | ||
| + | |||
| + | La fiecare actualizare (de ex miscarea personajului) trebuie facut broadcast. | ||
| + | |||
| + | <code> | ||
| + | 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); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | |||
| + | |||
| + | 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 | ||
| + | |||
| + | <code> | ||
| + | e.Client.MessageReceived += ChatMessageReceived; | ||
| + | </code> | ||
| + | |||
| + | apoi sa ascultati mesajele si se face broadcast | ||
| + | |||
| + | <code> | ||
| + | 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); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | |||
| + | ==== Deconectarea ===== | ||
| + | <code> | ||
| + | |||
| + | 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); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | | ||
| + | </code> | ||