Despre modelul de rețea în jocuri pentru începători

Despre modelul de rețea în jocuri pentru începători
În ultimele două săptămâni, am lucrat la un motor de rețea pentru jocul meu. Până acum, nu știam nimic despre tehnologiile de rețea în jocuri, așa că am citit o mulțime de articole și am efectuat numeroase experimente pentru a înțelege toate conceptele și pentru a putea scrie propriul meu motor de rețea.

În acest ghid, aș dori să împărtășesc cu voi diferitele concepte pe care trebuie să le învățați înainte de a scrie propriul motor de joc, precum și cele mai bune resurse și articole pentru studierea lor.

În general, există două tipuri principale de arhitecturi de rețea: peer-to-peer și client-server. În arhitectura peer-to-peer (p2p), datele sunt transmise între orice pereche de jucători conectați, iar în arhitectura client-server, datele sunt transmise doar între jucători și server.

Deși arhitectura peer-to-peer este încă utilizată în unele jocuri, standardul este arhitectura client-server: aceasta este mai simplu de implementat, necesită o lățime de bandă mai mică și facilitează protecția împotriva trișatului. Prin urmare, în acest ghid ne vom concentra pe arhitectura client-server.

În special, ne interesează serverele autoritative: în astfel de sisteme, serverul are întotdeauna dreptate. De exemplu, dacă un jucător crede că se află în coordonatele (10, 5), iar serverul îi spune că este în (5, 3), clientul trebuie să-și actualizeze poziția cu cea furnizată de server, și nu invers. Utilizarea serverelor autoritative facilitează identificarea trișorilor.

În sistemele de rețea pentru jocuri există trei componente principale:

  • Protocol de transport: cum sunt transmise datele între clienți și server.
  • Protocol de aplicație: ce este transmis de la clienți la server și de la server la clienți și în ce format.
  • Logica aplicației: cum sunt utilizate datele transmise pentru a actualiza starea clienților și a serverului.

Este foarte important să înțelegem rolul fiecărei părți și dificultățile aferente.

Protocol de transport

Primul pas este alegerea unui protocol pentru transportul datelor între server și clienți. Există două protocoale de Internet pentru aceasta: TCP și UDP. Dar poți crea și propriul tău protocol de transport bazat pe unul dintre ele sau poți folosi o bibliotecă în care sunt folosite.

Compararea TCP și UDP

Atât TCP, cât și UDP se bazeză pe IPIP permite transmiterea unui pachet de la sursă la destinatar, dar nu oferă garanții că pachetul trimis va ajunge în cele din urmă la destinatar, că va ajunge la el măcar o dată și că secvența pachetelor va veni în ordinea corectă. Mai mult, pachetul poate conține doar o dimensiune limitată de date, determinată de mărimea MTU.

UDP este doar un strat subțire deasupra IP. Prin urmare, are aceleași limitări. Spre deosebire de acesta, TCP are o mulțime de caracteristici. Oferă o conexiune fiabilă și ordonată între două noduri, cu verificare a erorilor. Prin urmare, TCP este foarte convenabil și este utilizat în multe alte protocoale, de exemplu, în HTTP, FTP și SMTP. Dar toate aceste funcții au un preț: întârziere.

Pentru a înțelege de ce aceste funcții pot provoca întârzieri, trebuie să înțelegem cum funcționează TCP. Atunci când un nod trimite un pachet unui nod destinatar, se așteaptă să primească o confirmare (ACK). Dacă, după un anumit timp, nu o primește (deoarece pachetul sau confirmarea au fost pierdute sau din alte motive), trimite din nou pachetul. În plus, TCP garantează primirea pachetelor în ordinea corectă, așa că, până când pachetul pierdut nu este primit, toate celelalte pachete nu pot fi procesate, chiar dacă au fost deja primite de nodul destinatar.

Dar, așa cum probabil înțelegeți, întârzierile în jocurile multiplayer sunt foarte importante, mai ales în genuri active precum FPS. De aceea, multe jocuri folosesc UDP cu un protocol propriu.

Protocolul propriu bazat pe UDP poate fi mai eficient decât TCP din diverse motive. De exemplu, poate marca anumite pachete ca fiind fiabile, iar altele ca fiind nesigure. Prin urmare, nu îl interesează dacă pachetul nesigur a ajuns la destinatar. Sau poate gestiona mai multe fluxuri de date, pentru a evita ca un pachet pierdut dintr-un flux să încetinească celelalte fluxuri. De exemplu, poate exista un flux pentru intrarea jucătorului și un alt flux pentru mesajele de chat. Dacă un mesaj de chat, care nu este o dată urgentă, este pierdut, atunci nu va încetini procesul de intrare, care este urgent. Sau protocolul propriu poate realiza fiabilitatea diferit față de TCP, pentru a fi mai eficient în condițiile jocurilor video.

Așadar, dacă TCP este atât de slab, vom crea un protocol de transport bazat pe UDP?

Situația este un pic mai complicată. Chiar dacă TCP este aproape suboptim pentru sistemele de rețea de jocuri, el poate funcționa destul de bine în jocul tău și să îți economisească timpul prețios. De exemplu, întârzierea nu poate fi o problemă pentru un joc pe ture sau pentru jocuri care se pot juca doar în rețele LAN, unde întârzierea și pierderile de pachete sunt mult mai reduse decât pe internet.

Multe jocuri de succes, inclusiv World of Warcraft, Minecraft și Terraria, folosesc TCP. Totuși, majoritatea jocurilor FPS aplică protocoale proprii bazate pe UDP, așa că mai jos vom discuta despre ele în detaliu.

Dacă decizi să folosești TCP, asigură-te că este dezactivat algoritmul Nagle, deoarece acesta bufferizează pachetele înainte de a le trimite, ceea ce înseamnă că crește întârzierea.

Pentru a afla mai multe despre diferențele dintre UDP și TCP în contextul jocurilor multiplayer, poți citi articolul lui Glenn Fiedler UDP vs. TCP.

Protocol propriu

Așadar, vrei să creezi un protocol de transport propriu, dar nu știi de unde să începi? Ai noroc, deoarece Glenn Fiedler a scris două articole minunate despre acest subiect. În ele vei găsi multe idei ingenioase.

Primul articol, Networking for Game Programmers din 2008, este mai simplu decât al doilea, Building A Game Network Protocol din 2016. Îți recomand să începi cu cel mai vechi.

Ține cont că Glenn Fiedler este un susținător important al utilizării propriului protocol bazat pe UDP. Și după ce citești articolele sale, este foarte posibil să adopți și tu opinia că TCP are dezavantaje serioase în jocurile video și să dorești să implementezi propriul tău protocol.

Dar dacă ești începător în lucrul cu rețelele, fă-ți un favor și folosește TCP sau o bibliotecă. Pentru a implementa cu succes un protocol de transport propriu, trebuie să înveți întâi multe lucruri.

Biblioteci de rețea

Dacă ai nevoie de ceva mai eficient decât TCP, dar nu vrei să te complici cu implementarea propriului protocol și să te enervezi cu o mulțime de detalii, poți folosi o bibliotecă de rețea. Există foarte multe:

Nu am încercat toate, dar prefer ENet pentru că este ușor de utilizat și fiabil. În plus, are documentație clară și tutoriale pentru începători.

Protocol de transport: concluzie

În concluzie: există două protocoale de transport principale: TCP și UDP. TCP are multe caracteristici utile: fiabilitate, menținerea ordinii pachetelor, detectarea erorilor. UDP nu are nimic din toate acestea, dar TCP are întârzieri mai mari, inacceptabile pentru unele jocuri. Așadar, pentru a asigura întârzieri scăzute, se poate crea un protocol personalizat bazat pe UDP sau se poate folosi o bibliotecă care implementează protocolul de transport pe baza UDP și care este adaptată pentru jocuri video multiplayer.

Alegerea între TCP, UDP și o bibliotecă depinde de mai mulți factori. În primul rând, de cerințele jocului: are nevoie de întârzieri scăzute? În al doilea rând, de cerințele protocolului aplicației: are nevoie de un protocol fiabil? Așa cum vom vedea în partea următoare, se poate crea un protocol de aplicație pentru care un protocol nesigur ar fi suficient. În final, trebuie să luăm în considerare și experiența dezvoltatorului motorului de rețea.

Am două sfaturi:

  • Abstrați cât mai mult protocolul de transport de restul aplicației, astfel încât să poată fi înlocuit cu ușurință fără a rescrie întregul cod.
  • Nu vă angajați în optimizări premature. Dacă nu sunteți specialist în rețele și nu sunteți sigur dacă aveți nevoie de un protocol de transport personalizat bazat pe UDP, puteți începe cu TCP sau cu o bibliotecă care asigură fiabilitate și apoi să testați și să măsurați performanța. Dacă apar probleme și sunteți sigur că motivele se regăsesc în protocolul de transport, atunci este posibil că a venit momentul să creați un protocol de transport personalizat.

În încheierea acestei părți, vă recomand să citiți Introducere în programarea jocurilor multiplayer de Brian Hook, în care sunt abordate multe dintre subiectele discutate aici.

Protocolul aplicației

Acum, când putem schimba date între clienți și server, trebuie să decidem ce date să transmitem și în ce format.

Schema clasică este că clienții trimit serverului input sau acțiuni, iar serverul trimite clienților starea actuală a jocului.

Serverul trimite un stat complet, ci un stat filtrat cu entitățile care se află aproape de jucător. Făcând acest lucru, acționează din trei motive. În primul rând, un stat complet poate fi prea mare pentru a fi transmis cu o frecvență mare. În al doilea rând, clienții sunt de obicei interesați de datele vizuale și audio, deoarece cea mai mare parte a logicii de joc este simulată pe server. În al treilea rând, în unele jocuri, jucătorul nu ar trebui să cunoască anumite date, cum ar fi poziția adversarului de la celălalt capăt al hărții, deoarece, altfel, ar putea analiza pachetele și să știe exact încotro să se îndrepte pentru a-l elimina.

Serializare

Primul pas va fi transformarea datelor pe care dorim să le trimitem (input sau starea jocului) într-un format potrivit pentru transmitere. Acest proces se numește serializare.

În mod instinctiv, ne vine în minte utilizarea unui format uman-readable, cum ar fi JSON sau XML. Dar acest lucru ar fi complet ineficient și ar ocupa în mod inutil o mare parte din canal.

În schimb, se recomandă utilizarea unui format binar, care este mult mai compact. Asta înseamnă că pachetele vor conține doar câțiva biți. Aici trebuie să avem în vedere problema ordinii biților, care poate varia pe diferite computere.

Pentru serializarea datelor, se poate folosi o bibliotecă, de exemplu:

Asigurați-vă doar că biblioteca creează arhive portabile și se ocupă de ordinea biților.

O soluție alternativă ar putea fi implementarea de sine stătătoare, care nu este foarte complicată, mai ales dacă în cod folosiți o abordare orientată pe date. În plus, va permite să efectuați optimizări care nu sunt întotdeauna posibile atunci când utilizați o bibliotecă.

Glenn Fidler a scris despre serializare două articole: Reading and Writing Packets și Serialization Strategies.

Compresie

Cantitatea de date transmise între clienți și server este limitată de lățimea de bandă a canalului. Comprimarea datelor va permite transmiterea mai multor date în fiecare instantaneu, va crește frecvența de actualizare sau pur și simplu va reduce cerințele canalului.

Pachetizarea pe biți

Prima tehnică este ambalarea pe biți. Aceasta constă în utilizarea exact a numărului de biți necesar pentru a descrie valoarea dorită. De exemplu, dacă aveți o enumerare care poate avea 16 valori diferite, în loc de un byte întreg (8 biți), puteți folosi doar 4 biți.

Glenn Fiddler explică cum să realizați acest lucru în a doua parte a articolului Reading and Writing Packets.

Ambalarea pe biți funcționează foarte bine cu discretizarea, care va fi tema următoarei secțiuni.

Discretizarea

Discretizarea este o tehnică de compresie cu pierderi, care constă în utilizarea doar a unui subset de valori posibile pentru a codifica o mărime. Cel mai simplu se poate realiza discretizarea prin rotunjirea numerelor zecimale.

Glenn Fiddler (din nou!) arată cum se aplică discretizarea în practică, în articolul său Snapshot Compression.

Algoritmi de compresie

Următoarea tehnică vor fi algoritmii de compresie fără pierderi.

Iată, în opinia mea, trei dintre cei mai interesanți algoritmi pe care trebuie să-i știți:

  • Codificarea Huffman cu cod precalculat, care este extrem de rapid și poate oferi rezultate bune. A fost utilizat pentru compresia pachetelor în motorul de rețea Quake3.
  • zlib este un algoritm de compresie de uz general, care nu crește niciodată volumul datelor. După cum se poate observa aici, a fost aplicat în numeroase domenii. Pentru actualizarea stărilor, poate deveni redundant. Dar poate fi util dacă trebuie să trimiteți clienților de pe server resurse, texte lungi sau relief.
  • Copierea lungilor serii este, probabil, cel mai simplu algoritm de compresie, dar este foarte eficient pentru anumite tipuri de date și poate fi folosit ca etapă de preprocesare înainte de zlib. Este deosebit de potrivit pentru compresia reliefului format din module sau voxeluri, unde multe elemente învecinate se repetă.

Compresia delta

Ultima metodă de compresie este compresia delta. Aceasta constă în transmiterea doar a diferențelor dintre starea actuală a jocului și ultima stare primită de client.

A fost aplicată pentru prima dată în motorul de rețea Quake3. Iată două articole care explică modul în care poate fi utilizată:

Glenn Fiedler a folosit-o și în a doua parte a articolului său Snapshot Compression.

Criptare

De asemenea, s-ar putea să fie necesar să criptați informațiile transmise între clienți și server. Există câteva motive pentru aceasta:

  • privatitate/confidențialitate: mesajele pot fi citite doar de către destinatar, iar nici o altă persoană care face monitorizarea rețelei nu va reuși să le citească.
  • autentificare: persoana care dorește să joace rolul unui jucător trebuie să cunoască cheia sa.
  • prevenirea înșelătoriei: va fi mult mai dificil pentru jucătorii rău intenționați să creeze propriile pachete pentru a trișa, va trebui să reproducă schema de criptare și să găsească cheia (care se schimbă la fiecare conexiune).

Recomand cu tărie utilizarea unei biblioteci pentru aceasta. Vă sugerez să folosiți libsodium, deoarece este deosebit de simplă și are tutoriale excelente. În mod special, tutorialul despre schimbul de chei, care permite generarea de chei noi la fiecare nouă conexiune.

Protocolul aplicației: concluzie

Aici ne încheiem cu protocolul aplicației. Consider că comprimarea nu este absolut necesară, iar decizia de a o utiliza depinde doar de joc și de lățimea de bandă necesară. Criptarea, din punctul meu de vedere, este obligatorie, dar în primul prototip se poate face fără ea.

Logica aplicației

Acum suntem capabili să actualizăm starea în client, dar ne putem confrunta cu probleme de întârziere. Jucătorul, după ce introduce o comandă, trebuie să aștepte actualizarea stării jocului de la server pentru a vedea ce impact a avut asupra lumii.

Mai mult, între două actualizări ale stării, lumea este complet statică. Dacă frecvența actualizărilor stării este scăzută, mișcările vor fi foarte sacadate.

Există câteva tehnici care pot diminua impactul acestei probleme, iar în secțiunea următoare voi vorbi despre ele.

Tehnici de netezire a întârziatei

Toate tehnicile descrise în această secțiune sunt detaliate într-o serie de Fast-Paced Multiplayer de Gabriel Gambetta. Recomand cu tărie citirea acestei serii excelente de articole. Există, de asemenea, o demonstrație interactivă care vă permite să vedeți cum funcționează aceste tehnici în practică.

Prima tehnică constă în aplicarea imediată a rezultatului introducerii, fără a aștepta un răspuns de la server. Aceasta se numește previziune pe partea clientului. Cu toate acestea, atunci când clientul primește o actualizare de la server, acesta trebuie să se asigure că prognoza sa a fost corectă. Dacă nu este așa, trebuie să-și schimbe pur și simplu starea conform celor primite de la server, deoarece serverul este autoritar. Această tehnică a fost folosită pentru prima dată în Quake. Mai multe detalii despre aceasta pot fi găsite în articol. Revizuirea codului Quake Engine Fabien Sanglard [traducere pe Habr.]

Al doilea set de tehnici este utilizat pentru a netezi mișcarea altor entități între două actualizări de stare. Există două metode de a aborda această problemă: interpolarea și extrapolarea. În cazul interpolării, se iau ultimele două stări și se arată tranziția între ele. Dezavantajul este că ea provoacă o mică întârziere, deoarece clientul vede întotdeauna ceea ce s-a întâmplat în trecut. Extrapolarea presupune prognozarea locului în care ar trebui să se afle acum entitățile pe baza ultimei stări primite de client. Dezavantajul este că, dacă entitatea își schimbă complet direcția de mișcare, va exista o mare eroare între prognoză și poziția reală.

Ultima, cea mai avansată tehnică, utilă doar în FPS — este compensarea întârzierii. Atunci când se folosește compensarea întârzierii, serverul ia în considerare întârzierile clientului atunci când acesta trage în țintă. De exemplu, dacă jucătorul a realizat un headshot pe ecranul său, dar în realitate ținta sa se afla într-un alt loc din cauza întârzierii, ar fi nedrept să refuzi jucătorului dreptul de a obține o ucidere din cauza întârzierii. Prin urmare, serverul derulează timpul înapoi, până în momentul în care jucătorul a tras, pentru a simula ceea ce a văzut jucătorul pe ecranul său și pentru a verifica coliziunea între focul său și țintă.

Glenn Fiedler (ca întotdeauna!) a scris în 2004 un articol Network Physics (2004), în care a pus bazele sincronizării simulării fizicii între server și client. În 2014, a scris o nouă serie de articole Networking Physics, în care a descris alte tehnici pentru sincronizarea simulării fizicii.

De asemenea, în wiki-ul companiei Valve există două articole, Source Multiplayer Networking și Metode de Compensare a Întârzierilor în Proiectarea și Optimizarea Protocolului Client/Server în Jocuri , în care se discută despre compensarea întârzierilor.

Prevenirea înșelăciunii

Există două tehnici principale pentru prevenirea înșelăciunii.

Prima: complicarea trimiterii pachetelor dăunătoare de către jucători. Așa cum s-a menționat mai sus, o modalitate bună de a o implementa este criptarea.

A doua: serverul autoritar trebuie să primească doar comenzi/input/acțiuni. Clientul nu ar trebui să aibă posibilitatea de a modifica starea serverului decât prin trimiterea inputului. Apoi, serverul trebuie să verifice întotdeauna corectitudinea inputului înainte de al aplica.

Logica aplicației: concluzie

Îți recomand să implementezi un mod de simulare a întârzierilor mari și a frecvențelor de actualizare scăzute, pentru a putea testa comportamentul jocului tău în condiții nefavorabile, chiar și atunci când clientul și serverul sunt rulate pe aceeași mașină. Acest lucru va simplifica foarte mult implementarea tehnicilor de netezire a întârzierilor.

Alte resurse utile

Dacă vrei să explorezi alte resurse dedicate modelelor de rețea, le poți găsi aici:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster