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

Despre modelul de rețea în jocurile pentru începători
În ultimele două săptămâni am lucrat la motorul online pentru jocul meu. Înainte de asta, nu știam absolut nimic despre crearea de rețele în jocuri, așa că am citit o mulțime de articole și am făcut o mulțime de experimente pentru a înțelege toate conceptele și a putea să-mi scriu propriul motor de rețea.

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

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

Deși arhitectura peer-to-peer este încă folosită în unele jocuri, client-server este standardul: este mai ușor de implementat, necesită o lățime mai mică a canalului și îl face mai ușor de protejat împotriva înșelăciunii. Prin urmare, în acest tutorial ne vom concentra pe arhitectura client-server.

În special, ne interesează cel mai mult serverele autoritare: în astfel de sisteme, serverul are întotdeauna dreptate. De exemplu, dacă un jucător crede că este la coordonatele (10, 5), iar serverul îi spune că este la (5, 3), atunci clientul ar trebui să-și înlocuiască poziția cu cea raportată de server, și nu vice invers. Utilizarea serverelor autorizate facilitează identificarea trișorilor.

Sistemele de jocuri în rețea au trei componente principale:

  • Protocol de transport: cum sunt transferate datele între clienți și server.
  • Protocol de aplicație: ce se transmite de la clienți la server și de la server la clienți și în ce format.
  • Logica aplicației: modul în care datele transferate sunt utilizate pentru a actualiza starea clienților și a serverului.

Este foarte important să înțelegeți rolul fiecărei părți și provocările asociate cu acestea.

Protocolul de transport

Primul pas este selectarea unui protocol pentru transportul datelor între server și clienți. Există două protocoale de internet pentru aceasta: TCP и UDP. Dar vă puteți crea propriul protocol de transport pe baza unuia dintre ele sau puteți folosi o bibliotecă care le folosește.

Comparație între TCP și UDP

Atât TCP, cât și UDP se bazează pe IP. IP permite ca un pachet să fie transmis de la o sursă la un destinatar, dar nu garantează că pachetul trimis va ajunge mai devreme sau mai târziu la destinatar, că îl va ajunge cel puțin o dată și că secvența pachetelor va ajunge în modul corect. Ordin. Mai mult, un pachet poate conține doar o cantitate limitată de date, dată de valoare MTU.

UDP este doar un strat subțire deasupra IP-ului. Prin urmare, are aceleași limitări. În schimb, TCP are multe caracteristici. Oferă o conexiune fiabilă, ordonată între două noduri, cu verificarea erorilor. Prin urmare, TCP este foarte convenabil și este utilizat în multe alte protocoale, de ex. HTTP, FTP и SMTP. Dar toate aceste caracteristici au un preț: întârziere.

Pentru a înțelege de ce aceste funcții pot cauza latență, trebuie să înțelegem cum funcționează TCP. Când un nod expeditor transmite un pachet către un nod receptor, se așteaptă să primească o confirmare (ACK). Dacă după un anumit timp nu îl primește (pentru că pachetul sau confirmarea a fost pierdută sau din alt motiv), atunci retrimite pachetul. Mai mult, TCP garantează că pachetele sunt primite în ordinea corectă, astfel încât până la primirea pachetului pierdut, toate celelalte pachete nu pot fi procesate, chiar dacă au fost deja primite de gazda care primește.

Dar, după cum probabil vă puteți imagina, latența în jocurile multiplayer este foarte importantă, mai ales în genurile pline de acțiune, cum ar fi FPS. Acesta este motivul pentru care multe jocuri folosesc UDP cu propriul protocol.

Un protocol nativ bazat pe UDP poate fi mai eficient decât TCP din diverse motive. De exemplu, poate marca unele pachete ca fiind de încredere, iar altele ca nede încredere. Prin urmare, nu îi pasă dacă pachetul neîncrezat ajunge la destinatar. Sau poate procesa mai multe fluxuri de date, astfel încât un pachet pierdut într-un flux să nu încetinească fluxurile rămase. De exemplu, ar putea exista un fir pentru introducerea jucătorului și un alt fir pentru mesajele de chat. Dacă se pierde un mesaj de chat care nu este urgent, acesta nu va încetini introducerea care este urgentă. Sau un protocol proprietar ar putea implementa fiabilitatea diferit de TCP pentru a fi mai eficient într-un mediu de joc video.

Deci, dacă TCP e atât de dezastruos, atunci ne vom crea propriul protocol de transport bazat pe UDP?

E puțin mai complicat. Chiar dacă TCP este aproape suboptim pentru sistemele de rețea de jocuri, poate funcționa destul de bine pentru jocul dvs. specific și vă poate economisi timp prețios. De exemplu, latența poate să nu fie o problemă pentru un joc pe rând sau pentru un joc care poate fi jucat numai pe rețele LAN, unde latența și pierderea de pachete sunt mult mai mici decât pe Internet.

Multe jocuri de succes, inclusiv World of Warcraft, Minecraft și Terraria, folosesc TCP. Cu toate acestea, majoritatea FPS-urilor folosesc propriile protocoale bazate pe UDP, așa că vom vorbi mai multe despre ele mai jos.

Dacă decideți să utilizați TCP, asigurați-vă că este dezactivat algoritmul lui Nagle, deoarece pune în tampon pachetele înainte de trimitere, ceea ce înseamnă că crește latența.

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

Protocol propriu

Deci vrei să-ți creezi propriul protocol de transport, dar nu știi de unde să începi? Ai noroc pentru că Glenn Fiedler a scris două articole uimitoare despre asta. Veți găsi în ele o mulțime de gânduri inteligente.

Primul articol Rețea pentru programatori de jocuri 2008, mai ușor decât al doilea, Construirea unui protocol de rețea de jocuri 2016. Vă recomand să începeți cu cel mai vechi.

Rețineți că Glenn Fiedler este un mare susținător al utilizării unui protocol personalizat bazat pe UDP. Și după ce-i citești articolele, probabil că vei adopta părerea lui că TCP are deficiențe serioase în jocurile video și vei dori să implementezi propriul protocol.

Dar dacă sunteți nou în rețele, fă-ți un serviciu și folosește TCP sau o bibliotecă. Pentru a implementa cu succes propriul protocol de transport, trebuie să înveți multe în prealabil.

Biblioteci de rețea

Dacă aveți nevoie de ceva mai eficient decât TCP, dar nu doriți să treceți prin bătaia de cap de a implementa propriul protocol și de a intra în multe detalii, puteți utiliza o bibliotecă de rețea. Sunt foarte multe:

Nu le-am încercat pe toate, dar prefer ENet pentru că este ușor de folosit și de încredere. În plus, are o documentație clară și un tutorial pentru începători.

Protocol de transport: Concluzie

Pentru a rezuma: există două protocoale principale de transport: TCP și UDP. TCP are multe caracteristici utile: fiabilitate, păstrarea ordinii pachetelor, detectarea erorilor. UDP nu are toate acestea, dar TCP prin natura sa a crescut latența, ceea ce este inacceptabil pentru unele jocuri. Adică, pentru a asigura o latență scăzută, poți să-ți creezi propriul protocol bazat pe UDP sau să folosești o bibliotecă care implementează un protocol de transport pe UDP și este adaptată pentru jocuri video multiplayer.

Alegerea dintre TCP, UDP și bibliotecă depinde de mai mulți factori. În primul rând, din nevoile jocului: are nevoie de latență scăzută? În al doilea rând, din cerințele protocolului de aplicație: are nevoie de un protocol de încredere? După cum vom vedea în partea următoare, este posibil să se creeze un protocol de aplicație pentru care un protocol care nu are încredere este destul de potrivit. În cele din urmă, trebuie să luați în considerare și experiența dezvoltatorului motorului de rețea.

Am doua sfaturi:

  • Retrageți protocolul de transport din restul aplicației cât mai mult posibil, astfel încât să poată fi înlocuit cu ușurință fără a rescrie tot codul.
  • Nu supraoptimizați. Dacă nu sunteți un expert î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 o bibliotecă care oferă fiabilitate, apoi puteți testa și măsura performanța. Dacă apar probleme și sunteți încrezător că cauza este protocolul de transport, atunci poate fi timpul să vă creați propriul protocol de transport.

La sfârșitul acestei părți, vă recomand să citiți Introducere în programarea jocurilor multiplayer de Brian Hook, care acoperă multe dintre subiectele discutate aici.

Protocolul de aplicare

Acum că putem face schimb de date între clienți și server, trebuie să decidem ce date să transferăm și în ce format.

Schema clasică este că clienții trimit intrări sau acțiuni către server, iar serverul trimite clienților starea curentă a jocului.

Serverul trimite nu starea completă, ci o stare filtrată cu entități care sunt situate în apropierea jucătorului. Face asta din trei motive. În primul rând, starea completă poate fi prea mare pentru a fi transmisă la frecvență înaltă. În al doilea rând, clienții sunt interesați în principal de datele vizuale și audio, deoarece cea mai mare parte a logicii jocului este simulată pe serverul de joc. În al treilea rând, în unele jocuri jucătorul nu trebuie să cunoască anumite date, de exemplu, poziția inamicului de cealaltă parte a hărții, altfel poate adulmeca pachete și poate ști exact unde să se mute pentru a-l ucide.

Serializare

Primul pas este să convertim datele pe care dorim să le trimitem (intrare sau stare de joc) într-un format potrivit pentru transmitere. Acest proces se numește serializare.

Gândul care îmi vine imediat în minte este să folosiți un format care poate fi citit de om, cum ar fi JSON sau XML. Dar acest lucru va fi complet ineficient și va risipi cea mai mare parte a canalului.

Este recomandat să folosiți în schimb formatul binar, care este mult mai compact. Adică, pachetele vor conține doar câțiva octeți. Există o problemă de luat în considerare aici ordinea octetilor, care poate diferi pe diferite computere.

Pentru a serializa datele, puteți utiliza o bibliotecă, de exemplu:

Doar asigurați-vă că biblioteca creează arhive portabile și îi pasă de endianness.

O soluție alternativă este să o implementați singur; nu este deosebit de dificil, mai ales dacă utilizați o abordare centrată pe date a codului dvs. În plus, vă va permite să efectuați optimizări care nu sunt întotdeauna posibile atunci când utilizați biblioteca.

Glenn Fiedler a scris două articole despre serializare: Citirea și scrierea pachetelor и Strategii de serializare.

Comprimare

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

Ambalare biți

Prima tehnică este împachetarea biților. Constă în folosirea exactă a numărului de biți care sunt necesari pentru a descrie valoarea dorită. De exemplu, dacă aveți o enumerare care poate avea 16 valori diferite, atunci în loc de un octet întreg (8 biți), puteți utiliza doar 4 biți.

Glenn Fiedler explică cum să implementați acest lucru în a doua parte a articolului Citirea și scrierea pachetelor.

Ambalarea biților funcționează mai ales bine cu eșantionarea, care va fi subiectul secțiunii următoare.

Prelevarea de probe

Prelevarea de probe este o tehnică de compresie cu pierderi care utilizează doar un subset de valori posibile pentru a codifica o valoare. Cel mai simplu mod de a implementa discretizarea este rotunjirea numerelor în virgulă mobilă.

Glenn Fiedler (din nou!) arată cum să pună în practică eșantionarea în articolul său Compresie instantanee.

Algoritmi de compresie

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

Iată, după părerea mea, cei mai interesanți trei algoritmi pe care trebuie să-i cunoașteți:

  • Codare Huffman cu cod precalculat, care este extrem de rapid și poate produce rezultate bune. A fost folosit pentru a comprima pachete în motorul de rețea Quake3.
  • zlib este un algoritm de compresie de uz general care nu mărește niciodată cantitatea de date. Cum poți vedea aici, a fost folosit într-o varietate de aplicații. Poate fi redundant pentru actualizarea stărilor. Dar poate fi util dacă trebuie să trimiteți bunuri, texte lungi sau teren către clienți de pe server.
  • Copierea lungimii tirajelor - Acesta este probabil cel mai simplu algoritm de compresie, dar este foarte eficient pentru anumite tipuri de date și poate fi folosit ca pas de preprocesare înainte de zlib. Este deosebit de potrivit pentru comprimarea terenului compus din plăci sau voxeli în care se repetă multe elemente adiacente.

Compresie Delta

Ultima tehnică de compresie este compresia delta. Constă în faptul că se transmit doar diferențele dintre starea curentă a jocului și ultima stare primită de client.

A fost folosit pentru prima dată în motorul de rețea Quake3. Iată două articole care explică cum să-l folosești:

Glenn Fiedler l-a folosit și în a doua parte a articolului său Compresie instantanee.

Criptare

În plus, poate fi necesar să criptați transferul de informații între clienți și server. Există mai multe motive pentru aceasta:

  • confidențialitate/confidențialitate: mesajele pot fi citite doar de destinatar și nicio altă persoană care adulmecă rețeaua nu le va putea citi.
  • autentificare: o persoană care dorește să joace rolul unui jucător trebuie să-și cunoască cheia.
  • Prevenirea cheat-ului: va fi mult mai dificil pentru jucătorii rău intenționați să-și creeze propriile pachete de cheat, ei vor trebui să reproducă schema de criptare și să găsească cheia (care se schimbă cu fiecare conexiune).

Recomand cu tărie să folosiți o bibliotecă pentru asta. Vă sugerez să utilizați libsodiu, pentru că este deosebit de simplu și are tutoriale excelente. Deosebit de interesant este tutorialul despre schimb de chei, care vă permite să generați chei noi cu fiecare nouă conexiune.

Protocol de aplicare: Concluzie

Aceasta încheie protocolul nostru de aplicare. Cred că compresia este complet opțională și decizia de a o folosi depinde doar de joc și de lățimea de bandă necesară. Criptarea, după părerea mea, este obligatorie, dar la primul prototip te poți descurca fără ea.

Logica aplicației

Acum putem să actualizăm starea clientului, dar este posibil să întâmpinăm probleme de latență. Jucătorul, după ce a finalizat introducerea, trebuie să aștepte ca starea jocului să se actualizeze de pe server pentru a vedea ce impact a avut asupra lumii.

Mai mult, între două actualizări de stat, lumea este complet statică. Dacă rata de actualizare a stării este scăzută, atunci mișcările vor fi foarte sacadate.

Există mai multe tehnici pentru a reduce impactul acestei probleme și le voi acoperi în secțiunea următoare.

Tehnici de netezire a latenței

Toate tehnicile descrise în această secțiune sunt discutate în detaliu în serie Multiplayer cu ritm rapid Gabriel Gambetta. Recomand cu căldură să citești această serie excelentă de articole. Include, de asemenea, o demonstrație interactivă care vă permite să vedeți cum funcționează aceste tehnici în practică.

Prima tehnică este să aplicați rezultatul de intrare direct, fără a aștepta un răspuns de la server. Se numeste prognoza la nivelul clientului. Cu toate acestea, atunci când clientul primește o actualizare de la server, acesta trebuie să verifice dacă predicția sa a fost corectă. Dacă nu este cazul, atunci trebuie doar să-și schimbe starea în funcție de ceea ce a primit de la server, deoarece serverul este autoritar. Această tehnică a fost folosită pentru prima dată în Quake. Puteți citi mai multe despre el în articol Revizuirea codului Quake Engine Fabien Sanglars [traducere pe Habré].

Al doilea set de tehnici este folosit pentru a ușura mișcarea altor entități între două actualizări de stare. Există două moduri de a rezolva această problemă: interpolare și extrapolare. În cazul interpolării, se iau ultimele două stări și se arată trecerea de la una la alta. Dezavantajul său este că provoacă o mică întârziere, deoarece clientul vede întotdeauna ce s-a întâmplat în trecut. Extrapolarea se referă la prezicerea unde ar trebui să fie acum entitățile pe baza ultimei stări primite de client. Dezavantajul său este că, dacă entitatea schimbă complet direcția de mișcare, atunci va exista o eroare mare între prognoză și poziția reală.

Cea mai recentă, cea mai avansată tehnică utilă doar în FPS este compensare lag. Când se utilizează compensarea lagului, serverul ia în considerare întârzierile clientului atunci când trage la țintă. De exemplu, dacă un jucător a executat o lovitură în cap pe ecran, dar, în realitate, ținta sa se afla într-o locație diferită din cauza întârzierii, atunci ar fi nedrept să îi refuzi jucătorului dreptul de a ucide din cauza întârzierii. Prin urmare, serverul derulează timpul înapoi până la momentul în care jucătorul a tras pentru a simula ceea ce jucătorul a văzut pe ecran și pentru a verifica dacă există o coliziune între împușcarea sa și țintă.

Glenn Fiedler (ca întotdeauna!) a scris un articol în 2004 Fizica rețelei (2004), în care a pus bazele pentru sincronizarea simulărilor de fizică între server și client. În 2014 a scris o nouă serie de articole Fizica rețelelor, care a descris alte tehnici de sincronizare a simulărilor fizice.

Există, de asemenea, două articole pe wiki-ul Valve, Sursă Multiplayer Networking и Metode de compensare a latenței în proiectarea și optimizarea protocolului client/server în joc care au în vedere compensarea pentru întârzieri.

Prevenirea înșelăciunii

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

În primul rând: îngreunarea trișorilor să trimită pachete rău intenționate. După cum am menționat mai sus, o modalitate bună de a implementa acest lucru este criptarea.

În al doilea rând: un server autoritar ar trebui să primească doar comenzi/input/acțiuni. Clientul nu ar trebui să poată schimba starea pe server decât prin trimiterea intrării. Apoi, de fiecare dată când serverul primește intrare, trebuie să verifice dacă este valid înainte de a-l folosi.

Logica de aplicare: concluzie

Vă recomand să implementați o modalitate de a simula latențe mari și rate de reîmprospătare scăzute, astfel încât să puteți testa comportamentul jocului în condiții proaste, chiar și atunci când clientul și serverul rulează pe același computer. Acest lucru va simplifica foarte mult implementarea tehnicilor de netezire a întârzierilor.

Alte resurse utile

Dacă doriți să explorați alte resurse despre modelele de rețea, le puteți găsi aici:

Sursa: www.habr.com

Adauga un comentariu