Om netværksmodellen i spil for begyndere

Om netværksmodellen i spil for begyndere
I de sidste to uger har jeg arbejdet på netværksmotoren til mit spil. Før det vidste jeg slet ikke noget om netværk i spil, så jeg læste en masse artikler og lavede en masse eksperimenter for at forstå alle begreberne og kunne skrive min egen netværksmotor.

I denne guide vil jeg gerne dele med dig de forskellige begreber, du skal lære, før du skriver din egen spilmotor, samt de bedste ressourcer og artikler til at lære dem.

Generelt er der to hovedtyper af netværksarkitekturer: peer-to-peer og klient-server. I en peer-to-peer (p2p)-arkitektur overføres data mellem ethvert par forbundne afspillere, mens der i en klient-server-arkitektur kun overføres data mellem spillerne og serveren.

Selvom peer-to-peer-arkitekturen stadig bruges i nogle spil, er klient-server standarden: den er nemmere at implementere, kræver en mindre kanalbredde og gør den nemmere at beskytte mod snyd. Derfor vil vi i denne guide fokusere på klient-server-arkitekturen.

Især er vi mest interesserede i autoritære servere: I sådanne systemer har serveren altid ret. For eksempel, hvis spilleren tror, ​​han er på (10, 5), og serveren fortæller ham, at han er på (5, 3), så skal klienten erstatte sin position med den, serveren rapporterer, ikke omvendt. Brugen af ​​autoritative servere gør det nemmere at genkende snydere.

Der er tre hovedkomponenter i spilnetværkssystemer:

  • Transportprotokol: hvordan data overføres mellem klienter og serveren.
  • Applikationsprotokol: hvad der transmitteres fra klienter til serveren og fra serveren til klienter, og i hvilket format.
  • Applikationslogik: hvordan de overførte data bruges til at opdatere klienternes og serverens tilstand.

Det er meget vigtigt at forstå hver enkelt dels rolle og de vanskeligheder, der er forbundet med dem.

Transportprotokol

Det første trin er at vælge en protokol til transport af data mellem serveren og klienterne. Der er to internetprotokoller til dette: TCP и UDP. Men du kan oprette din egen transportprotokol baseret på en af ​​dem eller bruge et bibliotek, der bruger dem.

Sammenligning af TCP og UDP

Både TCP og UDP er baseret på IP. IP tillader, at en pakke overføres fra en kilde til en modtager, men det garanterer ikke, at den sendte pakke vil nå frem til modtageren før eller siden, at den kommer til den mindst én gang, og at sekvensen af ​​pakker vil ankomme i den rigtige rækkefølge. Desuden kan en pakke kun indeholde en begrænset datastørrelse, givet af værdien MTU.

UDP er kun et tyndt lag oven på IP. Derfor har den de samme begrænsninger. I modsætning hertil har TCP mange funktioner. Det giver en pålidelig ordnet forbindelse mellem to noder med fejlkontrol. Derfor er TCP meget praktisk og bruges i mange andre protokoller, f.eks HTTP, FTP и SMTP. Men alle disse funktioner har en pris: forsinke.

For at forstå, hvorfor disse funktioner kan forårsage latency, er vi nødt til at forstå, hvordan TCP virker. Når den afsendende vært sender en pakke til den modtagende vært, forventer den at modtage en bekræftelse (ACK). Hvis den efter en vis tid ikke modtager den (fordi pakken eller bekræftelsen er gået tabt, eller af en anden grund), så sender den pakken igen. Desuden garanterer TCP, at pakker modtages i den rigtige rækkefølge, så indtil en tabt pakke er modtaget, kan alle andre pakker ikke behandles, selvom de allerede er modtaget af den modtagende node.

Men som du sikkert forstår, er latency i multiplayer-spil meget vigtig, især i så aktive genrer som FPS. Derfor bruger mange spil UDP med sin egen protokol.

En indbygget protokol baseret på UDP kan være mere effektiv end TCP af forskellige årsager. For eksempel kan den markere nogle pakker som betroede og andre som ikke-pålidelige. Derfor er han ligeglad med, om den upålidelige pakke er nået frem til modtageren. Eller den kan behandle flere datastrømme, så en pakke tabt i én strøm ikke bremser andre strømme. For eksempel kan der være en tråd til spillerinput og en anden tråd til chatbeskeder. Hvis en chatbesked, der ikke er hastedata, går tabt, vil den ikke bremse det input, der haster. Eller en proprietær protokol kan implementere pålidelighed anderledes end TCP for at være mere effektiv i et videospilsmiljø.

Så hvis TCP stinker, så bygger vi vores egen transportprotokol baseret på UDP?

Alt er lidt mere kompliceret. Selvom TCP næsten er suboptimalt til spilnetværkssystemer, kan det fungere ganske godt til dit specifikke spil og spare dig for værdifuld tid. For eksempel er latency muligvis ikke et problem for et turbaseret spil eller et spil, der kun kan spilles på LAN-netværk, hvor latency og pakketab er meget mindre end på internettet.

Mange succesrige spil, inklusive World of Warcraft, Minecraft og Terraria, bruger TCP. De fleste FPS'er bruger dog deres egne UDP-baserede protokoller, så vi vil tale mere om dem nedenfor.

Hvis du vælger at bruge TCP, så sørg for, at det er deaktiveret Nagles algoritme, fordi det buffer pakker før afsendelse, hvilket betyder, at det øger forsinkelsen.

For at lære mere om forskellene mellem UDP og TCP i forbindelse med multiplayer-spil, se Glenn Fiedlers artikel UDP vs. TCP.

Proprietær protokol

Så du vil oprette din egen transportprotokol, men ved ikke, hvor du skal starte? Du er heldig, for Glenn Fiedler skrev to fantastiske artikler om det. Du vil finde en masse smarte ideer i dem.

Første artikel Netværk for spilprogrammører 2008, lettere end den anden Opbygning af en spilnetværksprotokol 2016. Jeg anbefaler, at du starter med den ældre.

Vær opmærksom på, at Glenn Fiedler er en stor fortaler for at bruge din egen protokol baseret på UDP. Og efter at have læst hans artikler, vil du sandsynligvis vedtage hans mening om, at TCP har alvorlige ulemper i videospil, og du vil gerne implementere din egen protokol.

Men hvis du er ny til netværk, så gør dig selv en tjeneste og brug TCP eller et bibliotek. For at implementere din egen transportprotokol med succes skal du lære meget på forhånd.

Netværksbiblioteker

Hvis du har brug for noget mere effektivt end TCP, men ikke gider at implementere din egen protokol og gå ind i en masse detaljer, kan du bruge netbiblioteket. Der er mange af dem:

Jeg har ikke prøvet dem alle, men jeg foretrækker ENet, fordi det er nemt at bruge og pålideligt. Derudover har den tydelig dokumentation og en tutorial for begyndere.

Transportprotokol konklusion

For at opsummere er der to hovedtransportprotokoller: TCP og UDP. TCP har mange nyttige funktioner: pålidelighed, bevarelse af pakkerækkefølge, fejlfinding. UDP har ikke alt det, men TCP har i sagens natur høj latenstid, som er uacceptabel for nogle spil. Det vil sige, for at sikre lav latency, kan du oprette din egen protokol baseret på UDP eller bruge et bibliotek, der implementerer transportprotokollen på UDP og er tilpasset til multiplayer videospil.

Valget mellem TCP, UDP og biblioteket afhænger af flere faktorer. For det første fra spillets behov: har det brug for lav latenstid? For det andet, ud fra kravene i applikationsprotokollen: har den brug for en pålidelig protokol? Som vi vil se i næste del, er det muligt at oprette en applikationsprotokol, som en upålidelig protokol er ret egnet til. Endelig skal du også overveje netværksmotorudviklerens erfaring.

Jeg har to tips:

  • Abstraher transportprotokollen så meget som muligt fra resten af ​​applikationen, så den nemt kan udskiftes uden at omskrive al koden.
  • Overoptimer ikke. Hvis du ikke er netværksekspert og ikke er sikker på, om du har brug for din egen UDP-baserede transportprotokol, kan du starte med TCP eller et bibliotek, der giver pålidelighed, og derefter teste og måle ydeevne. Hvis du har problemer, og du er sikker på, at det er en transportprotokol, så er det måske på tide at oprette din egen transportprotokol.

I slutningen af ​​denne del anbefaler jeg, at du læser Introduktion til multiplayer spilprogrammering Brian Hook, som dækker mange af de emner, der diskuteres her.

Ansøgningsprotokol

Nu hvor vi kan udveksle data mellem klienter og serveren, skal vi beslutte, hvilke data der skal overføres og i hvilket format.

Den klassiske ordning er, at klienter sender input eller handlinger til serveren, og serveren sender den aktuelle spiltilstand til klienterne.

Serveren sender ikke den fulde, men den filtrerede tilstand med enheder, der er i nærheden af ​​afspilleren. Det gør han af tre grunde. For det første kan den samlede tilstand være for stor til at transmittere ved en høj frekvens. For det andet er klienter primært interesserede i visuelle og lyddata, fordi det meste af spillogikken er simuleret på spilserveren. For det tredje behøver spilleren i nogle spil ikke at kende visse data, såsom fjendens position på den anden side af kortet, fordi han ellers kan snuse til pakker og vide præcis, hvor han skal bevæge sig for at dræbe ham.

Serialisering

Det første trin er at konvertere de data, vi ønsker at sende (input eller spiltilstand), til et format, der er egnet til transmission. Denne proces kaldes serialisering.

Tanken kommer straks til at tænke på at bruge et menneskelæsbart format, såsom JSON eller XML. Men dette vil være fuldstændig ineffektivt og vil optage det meste af kanalen for ingenting.

I stedet anbefales det at bruge det binære format, som er meget mere kompakt. Det vil sige, at pakkerne kun vil indeholde nogle få bytes. Her skal vi tage højde for problemet byte rækkefølge, som kan variere på forskellige computere.

For at serialisere data kan du bruge et bibliotek, for eksempel:

Bare sørg for, at biblioteket opretter bærbare arkiver og tager sig af endianness.

En alternativ løsning ville være at implementere det selv, det er ikke så svært, især hvis du bruger en datacentreret tilgang i din kode. Derudover vil det give dig mulighed for at udføre optimeringer, som ikke altid er mulige, når du bruger biblioteket.

Glenn Fiedler har skrevet to artikler om serialisering: Læse- og skrivepakker и Serialiseringsstrategier.

kompression

Mængden af ​​data, der overføres mellem klienter og serveren, er begrænset af kanalens båndbredde. Datakomprimering giver dig mulighed for at overføre flere data i hvert snapshot, øge opdateringshastigheden eller blot reducere båndbreddekravene.

Bit pakning

Den første teknik er bitpakning. Det består i at bruge præcis det antal bits, der er nødvendigt for at beskrive den ønskede værdi. For eksempel, hvis du har en enum, der kan have 16 forskellige værdier, så kan du i stedet for en hel byte (8 bit) kun bruge 4 bit.

Glenn Fiedler forklarer, hvordan man implementerer dette i anden del af artiklen. Læse- og skrivepakker.

Bitpakning fungerer særligt godt med diskretisering, som vil være emnet for næste afsnit.

Prøveudtagning

Prøveudtagning er en tabsgivende komprimeringsteknik, der kun bruger en delmængde af mulige værdier til at kode en værdi. Den nemmeste måde at implementere diskretisering på er ved at afrunde flydende kommatal.

Glenn Fiedler viser (igen!) hvordan man anvender diskretisering i praksis i sin artikel Snapshot-komprimering.

Kompressionsalgoritmer

Den næste teknik vil være tabsfri komprimeringsalgoritmer.

Her er efter min mening de tre mest interessante algoritmer, som du har brug for at kende:

  • Huffman kodning med forudberegnet kode, som er ekstremt hurtig og kan give gode resultater. Det blev brugt til at komprimere pakker i Quake3-netværksmotoren.
  • zlib er en generel komprimeringsalgoritme, der aldrig øger mængden af ​​data. Hvordan kan du se her, er det blevet brugt i en række forskellige applikationer. For opdateringstilstande kan det være overflødigt. Men det kan være nyttigt, hvis du skal sende aktiver, lange tekster eller terræn til klienter fra serveren.
  • Kopiering af kørselslængder er nok den enkleste komprimeringsalgoritme, men den er meget effektiv til visse typer data og kan bruges som et forbehandlingstrin før zlib. Den er især velegnet til at komprimere terræn bestående af fliser eller voxels, hvor mange naboelementer gentages.

delta kompression

Den sidste kompressionsteknik er deltakompression. Det ligger i det faktum, at kun forskellene mellem den aktuelle spiltilstand og den sidste tilstand modtaget af klienten overføres.

Det blev først brugt i Quake3-netværksmotoren. Her er to artikler, der forklarer, hvordan du bruger det:

Glenn Fiedler brugte det også i anden del af sin artikel. Snapshot-komprimering.

kryptering

Derudover skal du muligvis kryptere transmissionen af ​​information mellem klienter og serveren. Det er der flere grunde til:

  • Fortrolighed/fortrolighed: Beskeder kan kun læses af modtageren, og ingen andre netværkssniffer vil være i stand til at læse dem.
  • autentificering: en person, der ønsker at spille rollen som en spiller, skal kende sin nøgle.
  • snydeforebyggelse: det vil være meget sværere for ondsindede spillere at oprette deres egne snydepakker, de bliver nødt til at replikere krypteringsskemaet og finde nøglen (som ændres på hver forbindelse).

Jeg anbefaler stærkt at bruge et bibliotek til dette. Jeg foreslår at bruge libsodium, fordi det er særligt enkelt og har gode tutorials. Særligt interessant er tutorial om nøgleudveksling, som giver dig mulighed for at generere nye nøgler på hver ny forbindelse.

Ansøgningsprotokol: Konklusion

Dette afslutter applikationsprotokollen. Jeg mener, at komprimering er helt valgfri, og beslutningen om at bruge den afhænger kun af spillet og den nødvendige båndbredde. Kryptering er efter min mening obligatorisk, men i den første prototype kan du undvære det.

Applikationslogik

Vi er nu i stand til at opdatere tilstanden i klienten, men vi kan løbe ind i latensproblemer. Spilleren skal, efter at have foretaget et input, vente på en spiltilstandsopdatering fra serveren for at se, hvilken effekt det har haft på verden.

Desuden er verden fuldstændig statisk mellem to tilstandsopdateringer. Hvis tilstandsopdateringshastigheden er lav, vil bevægelserne være meget rykke.

Der er flere teknikker til at afbøde virkningen af ​​dette problem, og jeg vil dække dem i næste afsnit.

Forsinkede udjævningsteknikker

Alle de teknikker, der er beskrevet i dette afsnit, er beskrevet detaljeret i serien. Hurtigt multiplayer Gabriel Gambetta. Jeg kan varmt anbefale at læse denne fremragende serie af artikler. Det inkluderer også en interaktiv demo for at se, hvordan disse teknikker fungerer i praksis.

Den første teknik er at anvende inputresultatet direkte uden at vente på et svar fra serveren. Det kaldes forudsigelse på klientsiden. Men når klienten modtager en opdatering fra serveren, skal den bekræfte, at dens forudsigelse var korrekt. Hvis dette ikke er tilfældet, så skal han bare ændre sin tilstand i henhold til hvad han har modtaget fra serveren, fordi serveren er autoritær. Denne teknik blev først brugt i Quake. Du kan læse mere om det i artiklen. Quake Engine kode gennemgang Fabien Sanglars [oversættelse på Habré].

Det andet sæt teknikker bruges til at udjævne bevægelsen af ​​andre enheder mellem to tilstandsopdateringer. Der er to måder at løse dette problem på: interpolation og ekstrapolation. I tilfælde af interpolation tages de to sidste tilstande, og overgangen fra den ene til den anden vises. Dens ulempe er, at det forårsager en lille brøkdel af forsinkelsen, fordi klienten altid ser, hvad der skete i fortiden. Ekstrapolation handler om at forudsige, hvor entiteterne nu skal være baseret på den sidste tilstand, som klienten har modtaget. Dens ulempe er, at hvis enheden fuldstændig ændrer bevægelsesretningen, vil der være en stor fejl mellem prognosen og den reelle position.

Den sidste, mest avancerede teknik, kun nyttig i FPS, er forsinkelseskompensation. Når du bruger forsinkelseskompensation, tager serveren hensyn til klientens forsinkelser, når den skyder mod målet. For eksempel, hvis en spiller udførte et hovedskud på deres skærm, men i virkeligheden var deres mål på et andet sted på grund af forsinkelsen, så ville det være uretfærdigt at nægte spilleren retten til at dræbe på grund af forsinkelsen. Så serveren spoler tiden tilbage til det tidspunkt, hvor spilleren skød, for at simulere, hvad spilleren så på deres skærm og tjekke for en kollision mellem deres skud og målet.

Glenn Fiedler (som altid!) skrev en artikel i 2004 Netværksfysik (2004), hvor han lagde grundlaget for synkroniseringen af ​​fysiksimuleringer mellem serveren og klienten. I 2014 skrev han en ny artikelserie netværksfysik, hvor han beskrev andre teknikker til synkronisering af fysiksimuleringer.

Der er også to artikler på Valves wiki, Kilde Multiplayer Networking и Latency-kompensationsmetoder i klient/server-protokoldesign og -optimering i spillet omhandler forsinkelseserstatning.

Forebyggelse af snyde

Der er to primære teknikker til forebyggelse af snyde.

For det første gør det sværere for snydere at sende ondsindede pakker. Som nævnt ovenfor er kryptering en god måde at implementere det på.

For det andet bør den autoritative server kun modtage kommandoer/input/handlinger. Klienten bør ikke være i stand til at ændre tilstanden på serveren andet end ved at sende input. Så skal serveren, hver gang den modtager input, tjekke den for gyldighed, før den anvender den.

Anvendelseslogik: Konklusion

Jeg anbefaler, at du implementerer en måde at simulere høj latens og lave opdateringshastigheder, så du kan teste dit spils adfærd under dårlige forhold, selv når klienten og serveren kører på den samme maskine. Dette forenkler i høj grad implementeringen af ​​forsinkelsesudjævningsteknikker.

Andre nyttige ressourcer

Hvis du vil udforske andre netværksmodelressourcer, kan du finde dem her:

Kilde: www.habr.com

Tilføj en kommentar