
I de sidste to uger har jeg arbejdet på en netværksmotor til mit spil. Før dette vidste jeg overhovedet ikke noget om netværksteknologier i spil, så jeg læste mange artikler og udførte en masse eksperimenter for at forstå alle koncepterne og være i stand til at skrive min egen netværksmotor.
I denne vejledning vil jeg gerne dele med dig forskellige koncepter, som du skal lære, inden 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 peer-to-peer-arkitekturen (p2p) overføres data mellem enhver par af tilsluttede spillere, mens data i klient-server-arkitekturen kun overføres mellem spillerne og serveren.
Selvom peer-to-peer-arkitekturen stadig anvendes i nogle spil, er standarden klient-server: den er lettere at implementere, kræver mindre båndbredde og gør det lettere at beskytte mod snyd. Derfor vil vi i denne vejledning fokusere på klient-server-arkitekturen.
Især er vi mest interesserede i autoritative servere: i sådanne systemer har serveren altid ret. For eksempel, hvis en spiller tror, at han er i koordinaterne (10, 5), men serveren fortæller ham, at han er i (5, 3), skal klienten erstatte sin position med den, som serveren sender, ikke omvendt. Brug af autoritative servere forenkler opdagelsen af snydere.
I spilnetværkssystemer er der tre grundlæggende komponenter:
- Transportprotokol: hvordan data overføres mellem klienter og server.
- Applikationsprotokol: hvad der overføres fra klienter til serveren og fra serveren til klienterne og i hvilken form.
- Applikationslogik: hvordan de overførte data bruges til at opdatere tilstanden for klienter og server.
Det er meget vigtigt at forstå hver del's rolle og de tilknyttede udfordringer.
Transportprotokol
Det første skridt er at vælge en protokol til transport af data mellem serveren og klienterne. Der findes to internetprotokoller til dette: og . Men du kan også oprette din egen transportprotokol baseret på en af dem eller bruge et bibliotek, hvor de bruges.
Sammenligning af TCP og UDP
Både TCP og UDP er baseret på . IP muliggør overførsel af pakker fra kilde til modtager, men giver ingen garantier for, at den sendte pakke når modtageren, eller at den vil nå ham en enkelt gang, eller at sekvensen af pakker ankommer i den rigtige rækkefølge. Desuden kan en pakke kun indeholde en begrænset mængde data, bestemt af størrelsen. .
UDP er blot et tyndt lag oven på IP. Derfor har det de samme begrænsninger. I modsætning hertil har TCP mange egenskaber. Det sikrer en pålidelig, ordnet forbindelse mellem to noder med fejlkontrol. Derfor er TCP meget praktisk og anvendes i mange andre protokoller, såsom i , og . Men alle disse funktioner har sin pris: .
For at forstå, hvorfor disse funktioner kan forårsage forsinkelse, skal man forstå, hvordan TCP fungerer. Når afsenders noden transmitterer en pakke til modtagers noden, forventer den at modtage en bekræftelse (ACK). Hvis den ikke modtager den inden for en bestemt tid (fordi pakken eller bekræftelsen er gået tabt, eller af andre grunde), sender den pakken igen. Desuden garanterer TCP, at pakker modtages i den rigtige rækkefølge, så indtil den tabte pakke er modtaget, kan alle andre pakker ikke behandles, selvom de allerede er modtaget af modtagers noden.
Men som du nok forstår, er forsinkelse i multiplayer-spil meget vigtig, især i aktive genrer som FPS. Derfor bruger mange spil UDP med deres egen protokol.
En egen protokol baseret på UDP kan være mere effektiv end TCP af forskellige grunde. For eksempel kan den markere nogle pakker som pålidelige og andre som upålidelige. Derfor er den ikke bekymret for, om den upålidelige pakke når modtageren. Eller den kan håndtere flere datastreams, så en pakke, der går tabt i én strøm, ikke forsinker de andre strømme. For eksempel kan der være én strøm til spillerens input og en anden strøm til chatbeskeder. Hvis en chatbesked, som ikke er presserende, går tabt, vil det ikke forsinke aktiviteten af input, som er presserende. Eller den egen protokol kan implementere pålidelighed anderledes end i TCP for at være mere effektiv i videospil.
Så, hvis TCP er så dårligt, skal vi så lave vores egen transportprotokol baseret på UDP?
Det er lidt mere kompliceret. Selvom TCP næsten er suboptimalt til spilnetværkssystemer, kan det faktisk fungere ret godt i dit spil og spare dig for værdifuld tid. For eksempel kan forsinkelse ikke være et problem for turbaserede spil eller spil, der kun spilles i LAN-netværk, hvor forsinkelser og pakke tab er meget mindre end på internettet.
I mange succesfulde spil, herunder World of Warcraft, Minecraft og Terraria, bruges TCP. Dog anvender de fleste FPS deres egne protokoller baseret på UDP, og derfor vil vi tale mere om dem nedenfor.
Hvis du beslutter dig for at bruge TCP, så sørg for, at er slået fra, fordi den buffrer pakker før afsendelse, hvilket øger forsinkelsen.
For at lære mere om forskellene mellem UDP og TCP i konteksten af multiplayer-spil, kan man læse Glenn Fiedlers artikel .
Egen protokol
Så du ønsker at lave din egen transportprotokol, men ved ikke, hvor du skal starte? Du er heldig, for Glenn Fiedler har skrevet to fantastiske artikler om dette. I dem finder du mange smarte tanker.
Den første artikel, fra 2008 er lettere end den anden, fra 2016. Jeg anbefaler, at du starter med den ældre.
Tag i betragtning, at Glenn Fiedler er en stor tilhænger af at bruge sin egen protokol baseret på UDP. Og efter at have læst hans artikler, vil du helt sikkert adoptere hans synspunkt om, at TCP har alvorlige ulemper i videospil, og du vil ønske at implementere din egen protokol.
Men hvis du er ny inden for 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 først.
Netværksbiblioteker
Hvis du har brug for noget mere effektivt end TCP, men ikke vil besvære dig med at implementere din egen protokol og dykke ned i mange detaljer, kan du bruge et netværksbibliotek. Der er mange offentlige:
- fra Glenn Fiedler
- , som ikke længere vedligeholdes, men dens fork tilsyneladende stadig er aktiv.
- — det er et bibliotek, der er skabt til multiplayer FPS
- fra Valve
Jeg har ikke prøvet dem alle, men jeg foretrækker ENet, fordi den er nem at bruge og pålidelig. Derudover har den en klar dokumentation og en tutorial for begyndere.
Transportprotokol: konklusion
For at opsummere: der findes to primære transportprotokoller: TCP og UDP. TCP har mange nyttige funktioner: pålidelighed, bevaring af pakkeorden, fejldetektering. UDP har ikke noget af dette, men TCP har naturligvis øgede forsinkelser, som er uacceptable for nogle spil. For at sikre lave forsinkelser kan man skabe en 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 af spillets behov: har den brug for lave forsinkelser? For det andet af applikationsprotokollens krav: har den brug for en pålidelig protokol? Som vi vil se i næste del, kan man skabe en applikationsprotokol, som sagtens kan bruge en upålidelig protokol. Endelig skal man tage højde for erfaringen hos udvikleren af netværksspilmotorn.
Jeg har to råd:
- Abstraher transportprotokollen så meget som muligt fra resten af applikationen, så den nemt kan udskiftes uden at skulle omskrive hele koden.
- Undgå forhastet optimering. Hvis du ikke er netværksspecialist og er usikker på, om du har brug for din egen transportprotokol baseret på UDP, kan du starte med TCP eller et bibliotek, der garanterer pålidelighed, og derefter teste og måle ydeevnen. Hvis der opstår problemer, og du er sikker på, at årsagen ligger i transportprotokollen, så kan det være tid til at skabe din egen transportprotokol.
Afslutningsvis anbefaler jeg, at du læser af Brian Hook, hvor mange af de emner, vi diskuterer her, bliver behandlet.
Applikationsprotokol
Nu hvor vi kan udveksle data mellem klienter og serveren, skal vi beslutte, hvilke data der skal sendes, og i hvilket format.
Den klassiske model indebærer, at klienterne sender input eller handlinger til serveren, og serveren sender det nuværende spilstatus til klienterne.
Serveren sender ikke en fuld, men en filtreret tilstand med entiteter, der er tættere på spilleren. Dette gør den af tre grunde. For det første kan den fulde tilstand være for stor til at sende med høj frekvens. For det andet er klienterne primært interesseret i visuelle og lyddata, fordi størstedelen af spillets logik simuleres på spillerserveren. For det tredje må spilleren i visse spil ikke vide om bestemte data, såsom modstanderens position i den anden ende af kortet, ellers kan de sniffes pakker og vide præcist, hvor de skal bevæge sig for at dræbe dem.
Serialisering
Det første skridt er at konvertere de data, vi ønsker at sende (input eller spilstilstand), til et format, der er egnet til transmission. Denne proces kaldes .
Man kommer straks til at tænke på at bruge et menneskeligt læsbart format, såsom JSON eller XML. Men det ville være helt ineffektivt og spild af en stor del af kanalen.
I stedet anbefales det at bruge et binært format, som er meget mere kompakt. Det vil sige, at pakkerne kun vil indeholde et par bytes. Her skal man tage højde for problemet med , som kan variere på forskellige computere.
Til serialisering af data kan man bruge et bibliotek, f.eks.:
- fra Google
- fra Sandstorm
- af Shayne Grant og Randolph Voorhies
Sørg blot for, at biblioteket skaber portable arkiver og tager sig af byteordenen.
En alternativ løsning kan være at implementere det selv; det er ikke særlig svært, især hvis du bruger en datadrevet tilgang i din kode. Desuden vil det give dig mulighed for at udføre optimeringer, som ikke altid er mulige ved brug af et bibliotek.
Glenn Fiddler har skrevet to artikler om serialisering: og .
Komprimering
Mængden af data, der overføres mellem klienter og server, er begrænset af kanalens båndbredde. Komprimering af data vil muliggøre overførsel af flere data i hver snapshot, øge opdateringsfrekvensen eller blot reducere kravene til kanalen.
Bit Packing
Den første teknik er bitpackning. Det involverer brugen af præcis det antal bits, der er nødvendige for at beskrive den ønskede størrelse. For eksempel, hvis du har en opremsning, som kan have 16 forskellige værdier, kan du i stedet for en hel byte (8 bits) kun bruge 4 bits.
Glenn Fiddler forklarer, hvordan man implementerer dette i den anden del af artiklen. .
Bitpackning fungerer især godt med diskretisering, som vil være emnet for næste afsnit.
Diskretisering
er en tabsgivende kompressionsteknik, der involverer brug af kun et underudvalg af mulige værdier til at kode en størrelse. Det er lettest at implementere diskretisering ved at rundetallene til nærliggende heltal.
Glenn Fiddler (igen!) viser, hvordan man anvender diskretisering i praksis i sin artikel. .
Komprimeringsalgoritmer
Den næste teknik vil være tabsfri komprimeringsalgoritmer.
Her er, efter min mening, tre af de mest interessante algoritmer, man skal kende:
- med forudberegnet kode, som er ekstremt hurtig og kan give gode resultater. Den blev brugt til at komprimere pakker i netværksmotoren Quake3.
- er en generel komprimeringsalgoritme, som aldrig øger datamængden. Som man kan se , blev den brugt i mange anvendelsesområder. For opdatering af tilstande kan den være overflødig. Men den kan også være nyttig, hvis du skal sende aktiver til kunder fra serveren, lange tekster eller terræn.
- er nok den simpleste komprimeringsalgoritme, men den er meget effektiv til bestemte typer data og kan bruges som et preprocessing-trin før zlib. Den er særlig velegnet til komprimering af terræn, der består af fliser eller voxeler, hvor mange nærliggende elementer gentages.
Delta-kompression
Den sidste komprimeringsmetode er delta-kompression. Det indebærer, at kun forskellene mellem den nuværende spiltilstand og den seneste tilstand modtaget fra klienten overføres.
Den blev første gang anvendt i netværksmotoren Quake3. Her er to artikler, der forklarer, hvordan den kan anvendes:
- Brian Hooks
- Fabien Sanglard [ artikler på Habré, se afsnittet "Netværksmodel"].
Glenn Fidler har også brugt det i den anden del af sin artikel .
Kryptering
Desuden kan du være nødt til at kryptere informationsoverførslen mellem klienter og serveren. Der er flere grunde til dette:
- privatliv/confidentialitet: beskeder kan kun læses af modtageren, og ingen andre, der udfører netværkssniffing, vil kunne læse dem.
- autentificering: en person, der ønsker at udfylde rollen som spiller, skal kende hans nøgle.
- forhindre snyd: ondsindede spillere vil have meget sværere ved at oprette deres egne pakker til snyd; de skal reproducere krypteringsskemaet og finde nøglen (som ændres ved hver forbindelse).
Jeg anbefaler stærkt at bruge et bibliotek til dette. Jeg foreslår at bruge , fordi det er særlig nemt at arbejde med og har fremragende tutorials. Særligt interessant er tutorialen om , som muliggør generering af nye nøgler ved hver ny forbindelse.
Applikationsprotokol: konklusion
Her slutter vi med applikationsprotokollen. Jeg mener, at kompression er helt unødvendig, og beslutningen om at bruge den afhænger udelukkende af spillet og den nødvendige båndbredde. Kryptering, efter min mening, er obligatorisk, men i den første prototype kan vi undvære det.
Applikationslogik
Nu kan vi opdatere status i klienten, men vi kan støde på forsinkelsesproblemer. Spilleren skal vente på statusopdateringen fra serveren for at se, hvilken indflydelse han har haft på verden efter at have indtastet.
Desuden er verden helt statisk mellem to statusopdateringer. Hvis opdateringsfrekvensen er lav, vil bevægelserne være meget hakkende.
Der er flere teknikker, der kan mindske indflydelsen af dette problem, og jeg vil tale om dem i næste afsnit.
Teknikker til at udglatte forsinkelser
Alle teknikkerne beskrevet i dette afsnit er blevet uddybet i serien af Gabriel Gam betta. Jeg anbefaler stærkt at læse denne fremragende artikelserie. Den indeholder også en interaktiv demo, der viser, hvordan disse teknikker fungerer i praksis.
Den første teknik er at anvende resultatet af indtastningen direkte uden at vente på et svar fra serveren. Dette kaldes klientside forudsigelseMen når klienten modtager en opdatering fra serveren, skal han sikre sig, at hans forudsigelse var korrekt. Hvis ikke, skal han blot ændre sin tilstand i henhold til det, han har modtaget fra serveren, da serveren er autoritativ. Denne teknik blev først anvendt i Quake. Læs mere om det i artiklen Fabien Sanglard [ på Habr].
Det andet sæt teknikker bruges til at glatte bevægelsen af andre enheder mellem to opdateringer af tilstand. Der er to måder at tackle denne opgave på: interpolation og ekstrapolation. I tilfælde af interpolation tages de to seneste tilstande, og overgangen fra en tilstand til en anden vises. Ulempen ved dette er, at det medfører en lille forsinkelse, da klienten altid ser, hvad der skete i fortiden. Ekstrapolation indebærer at forudsige, hvor enheder nu burde befinde sig baseret på den seneste tilstand, klienten har modtaget. Ulempen ved dette er, at hvis enheden helt ændrer retning, vil der opstå en stor fejlmargin mellem forudsigelsen og den faktiske position.
Den mest avancerede teknik, som kun er nyttig i FPS, er forsinkelseskompensation. Ved brug af forsinkelseskompensation tager serveren højde for klientens forsinkelser, når han sigter mod et mål. For eksempel, hvis spilleren udførte et headshot på sin skærm, men i virkeligheden var hans mål på grund af forsinkelse et andet sted, ville det være uretfærdigt at nægte spilleren retten til drab på grund af forsinkelsen. Derfor spoler serveren tiden tilbage til det tidspunkt, hvor spilleren skød, for at simulere, hvad spilleren så på sin skærm, og tjekke kollisionen mellem hans skud og målet.
Glenn Fidler (som altid!) skrev i 2004 en artikel , hvor han lagde fundamentet for synkronisering af fysiksimulering mellem serveren og klienten. I 2014 skrev han en ny serie artikler , der beskrev andre teknikker til synkronisering af fysiksimulering.
Derudover er der to artikler i Valves wiki, og , som omhandler forsinkelseskompensation.
Forebyggelse af snyd
Der er to hovedteknikker til at forhindre snyd.
Første: komplicering af afsendelse af ondsindede pakker af svindlere. Som nævnt før er en god måde at realisere dette på at anvende kryptering.
Anden: en autoritær server skal kun modtage kommandoer/input/handlinger. Klienten bør ikke have muligheder for at ændre status på serveren, bortset fra at sende input. Serveren skal ved hver modtagelse af input kontrollere det for gyldighed, inden det anvendes.
Applikationslogik: konklusion
Jeg anbefaler, at du implementerer en metode til at simulere store forsinkelser og lave opdateringsfrekvenser, så du kan teste din spils adfærd under dårlige forhold, selv når klient og server kører på den samme computer. Dette vil i høj grad forenkle implementeringen af forsinkelsessmootheringsteknikker.
Andre nyttige ressourcer
Hvis du ønsker at udforske andre ressourcer dedikeret til netværksmodeller, kan de findes her:
- — det er værd at læse hele hans blog, der er mange fremragende artikler. alle artikler om netværksteknologier er samlet.
- af M. Fatih MAR — det er en detaljeret liste over artikler og videoer om netværksmotorer til videospil.
- I har også mange nyttige links.
Kilde: habr.com
