Om nettverksmodellen i spill for nybegynnere

Om nettverksmodellen i spill for nybegynnere
De siste to ukene har jeg jobbet med nettmotoren for spillet mitt. Før dette visste jeg ingenting i det hele tatt om nettverk i spill, så jeg leste mange artikler og gjorde mange eksperimenter for å forstå alle konseptene og kunne skrive min egen nettverksmotor.

I denne guiden vil jeg gjerne dele med deg de ulike konseptene du må lære før du skriver din egen spillmotor, samt de beste ressursene og artiklene for å lære dem.

Generelt er det to hovedtyper nettverksarkitekturer: peer-to-peer og klient-server. I en peer-to-peer (p2p)-arkitektur overføres data mellom alle par tilkoblede spillere, mens i en klient-server-arkitektur overføres data kun mellom spillere og serveren.

Selv om peer-to-peer-arkitektur fortsatt brukes i noen spill, er klient-server standarden: den er enklere å implementere, krever en mindre kanalbredde og gjør det lettere å beskytte mot juks. Derfor vil vi i denne opplæringen fokusere på klient-server-arkitekturen.

Spesielt er vi mest interessert i autoritære servere: I slike systemer har serveren alltid rett. For eksempel, hvis en spiller tror han er på koordinatene (10, 5), og serveren forteller ham at han er på (5, 3), bør klienten erstatte sin posisjon med den som er rapportert av serveren, og ikke vice versa. Å bruke autoritative servere gjør det lettere å identifisere juksere.

Nettverksspillsystemer har tre hovedkomponenter:

  • Transportprotokoll: hvordan data overføres mellom klienter og server.
  • Applikasjonsprotokoll: hva som overføres fra klienter til server og fra server til klienter og i hvilket format.
  • Applikasjonslogikk: hvordan de overførte dataene brukes til å oppdatere tilstanden til klientene og serveren.

Det er svært viktig å forstå rollen til hver del og utfordringene knyttet til dem.

Transportprotokoll

Det første trinnet er å velge en protokoll for transport av data mellom serveren og klientene. Det er to Internett-protokoller for dette: TCP и UDP. Men du kan lage din egen transportprotokoll basert på en av dem eller bruke et bibliotek som bruker dem.

Sammenligning av TCP og UDP

Både TCP og UDP er basert på IP. IP tillater at en pakke overføres fra en kilde til en mottaker, men garanterer ikke at den sendte pakken før eller siden når mottakeren, at den vil nå den minst én gang, og at pakkesekvensen kommer i riktig rekkefølge. Dessuten kan en pakke bare inneholde en begrenset mengde data, gitt av verdien MTU.

UDP er bare et tynt lag på toppen av IP. Derfor har den de samme begrensningene. I kontrast har TCP mange funksjoner. Det gir en pålitelig, ryddig forbindelse mellom to noder med feilkontroll. Derfor er TCP veldig praktisk og brukes i mange andre protokoller, f.eks. HTTP, FTP и SMTP. Men alle disse funksjonene har en pris: forsinkelse.

For å forstå hvorfor disse funksjonene kan forårsake latens, må vi forstå hvordan TCP fungerer. Når en sendernode sender en pakke til en mottakernode, forventer den å motta en bekreftelse (ACK). Hvis den etter en viss tid ikke mottar den (fordi pakken eller bekreftelsen gikk tapt, eller av en annen grunn), sender den pakken på nytt. Dessuten garanterer TCP at pakker mottas i riktig rekkefølge, så inntil den tapte pakken er mottatt, kan ikke alle andre pakker behandles, selv om de allerede er mottatt av den mottakende verten.

Men som du sikkert kan forestille deg, er latency i flerspillerspill veldig viktig, spesielt i actionfylte sjangere som FPS. Dette er grunnen til at mange spill bruker UDP med sin egen protokoll.

En naturlig UDP-basert protokoll kan være mer effektiv enn TCP av ulike årsaker. Den kan for eksempel merke noen pakker som klarerte og andre som uklarerte. Derfor bryr det seg ikke om den uklarerte pakken når mottakeren. Eller den kan behandle flere datastrømmer slik at en tapt pakke i én strøm ikke bremser de gjenværende strømmene. Det kan for eksempel være en tråd for spillerinndata og en annen tråd for chatmeldinger. Hvis en chatmelding som ikke haster går tapt, vil den ikke bremse inndata som haster. Eller en proprietær protokoll kan implementere pålitelighet annerledes enn TCP for å være mer effektiv i et videospillmiljø.

Så hvis TCP suger så mye, vil vi lage vår egen transportprotokoll basert på UDP?

Det er litt mer komplisert. Selv om TCP er nesten suboptimalt for spillnettverkssystemer, kan det fungere ganske bra for ditt spesifikke spill og spare deg for verdifull tid. For eksempel kan latens ikke være et problem for et turbasert spill eller et spill som bare kan spilles på LAN-nettverk, der latens og pakketap er mye lavere enn på Internett.

Mange vellykkede spill, inkludert World of Warcraft, Minecraft og Terraria, bruker TCP. Imidlertid bruker de fleste FPS-er sine egne UDP-baserte protokoller, så vi snakker mer om dem nedenfor.

Hvis du bestemmer deg for å bruke TCP, sørg for at det er deaktivert Nagles algoritme, fordi den bufre pakker før sending, noe som betyr at den øker ventetiden.

For å lære mer om forskjellene mellom UDP og TCP i sammenheng med flerspillerspill, kan du lese Glenn Fiedlers artikkel UDP vs. TCP.

Egen protokoll

Så du vil lage din egen transportprotokoll, men vet ikke hvor du skal begynne? Du er heldig fordi Glenn Fiedler har skrevet to fantastiske artikler om dette. Du vil finne mange smarte tanker i dem.

Den første artikkelen Nettverk for spillprogrammerere 2008, enklere enn den andre, Bygge en spillnettverksprotokoll 2016. Jeg anbefaler at du starter med den eldre.

Merk at Glenn Fiedler er en stor talsmann for å bruke en tilpasset protokoll basert på UDP. Og etter å ha lest artiklene hans, vil du sannsynligvis vedta hans mening om at TCP har alvorlige mangler i videospill, og du vil ønske å implementere din egen protokoll.

Men hvis du er ny på nettverk, gjør deg selv en tjeneste og bruk TCP eller et bibliotek. For å lykkes med å implementere din egen transportprotokoll, må du lære mye på forhånd.

Nettverksbiblioteker

Hvis du trenger noe mer effektivt enn TCP, men ikke ønsker å gå gjennom bryet med å implementere din egen protokoll og gå inn i mange detaljer, kan du bruke et nettverksbibliotek. Det er mange av dem:

Jeg har ikke prøvd dem alle, men jeg foretrekker ENet fordi det er enkelt å bruke og pålitelig. I tillegg har den tydelig dokumentasjon og en veiledning for nybegynnere.

Transportprotokoll: Konklusjon

For å oppsummere: det er to hovedtransportprotokoller: TCP og UDP. TCP har mange nyttige funksjoner: pålitelighet, pakkeordrebevaring, feildeteksjon. UDP har ikke alt dette, men TCP har i sin natur økt latens, noe som er uakseptabelt for noen spill. Det vil si at for å sikre lav ventetid kan du lage din egen protokoll basert på UDP eller bruke et bibliotek som implementerer en transportprotokoll på UDP og er tilpasset flerspillervideospill.

Valget mellom TCP, UDP og biblioteket avhenger av flere faktorer. Først, fra behovene til spillet: trenger det lav latenstid? For det andre, fra applikasjonsprotokollkravene: trenger den en pålitelig protokoll? Som vi vil se i neste del, er det mulig å lage en applikasjonsprotokoll som en upålitelig protokoll er ganske egnet for. Til slutt må du også ta hensyn til erfaringen til nettverksmotorutvikleren.

Jeg har to råd:

  • Abstraher transportprotokollen fra resten av applikasjonen så mye som mulig slik at den enkelt kan erstattes uten å skrive om all koden.
  • Ikke overoptimaliser. Hvis du ikke er en nettverksekspert og ikke er sikker på om du trenger en tilpasset UDP-basert transportprotokoll, kan du starte med TCP eller et bibliotek som gir pålitelighet, og deretter teste og måle ytelse. Hvis det oppstår problemer og du er sikker på at årsaken er transportprotokollen, kan det være på tide å lage din egen transportprotokoll.

På slutten av denne delen anbefaler jeg at du leser Introduksjon til flerspillerspillprogrammering av Brian Hook, som dekker mange av emnene som diskuteres her.

Søknadsprotokoll

Nå som vi kan utveksle data mellom klienter og server, må vi bestemme hvilke data som skal overføres og i hvilket format.

Det klassiske opplegget er at klienter sender input eller handlinger til serveren, og serveren sender gjeldende spillstatus til klientene.

Serveren sender ikke hele tilstanden, men en filtrert tilstand med enheter som er plassert i nærheten av spilleren. Han gjør dette av tre grunner. For det første kan den fullstendige tilstanden være for stor til å kunne overføres med høy frekvens. For det andre er klienter hovedsakelig interessert i visuelle og lyddata, fordi det meste av spilllogikken er simulert på spillserveren. For det tredje, i noen spill trenger ikke spilleren å vite visse data, for eksempel posisjonen til fienden på den andre siden av kartet, ellers kan han snuse pakker og vite nøyaktig hvor han skal flytte for å drepe ham.

Serialisering

Det første trinnet er å konvertere dataene vi ønsker å sende (inndata eller spilltilstand) til et format som er egnet for overføring. Denne prosessen kalles serialisering.

Tanken som umiddelbart kommer til hjernen er å bruke et menneskelig lesbart format, for eksempel JSON eller XML. Men dette vil være helt ineffektivt og vil kaste bort det meste av kanalen.

Det anbefales å bruke det binære formatet i stedet, som er mye mer kompakt. Det vil si at pakkene bare vil inneholde noen få byte. Det er et problem å vurdere her byte rekkefølge, som kan variere på forskjellige datamaskiner.

For å serialisere data kan du bruke et bibliotek, for eksempel:

Bare sørg for at biblioteket lager bærbare arkiver og bryr seg om endianness.

En alternativ løsning er å implementere det selv; det er ikke spesielt vanskelig, spesielt hvis du bruker en datasentrisk tilnærming til koden din. I tillegg vil det tillate deg å utføre optimaliseringer som ikke alltid er mulig når du bruker biblioteket.

Glenn Fiedler skrev to artikler om serialisering: Lese- og skrivepakker и Serialiseringsstrategier.

Kompresjon

Mengden data som overføres mellom klienter og server er begrenset av kanalens båndbredde. Datakomprimering lar deg overføre mer data i hvert øyeblikksbilde, øke oppdateringsfrekvensen eller ganske enkelt redusere kanalkravene.

Bitemballasje

Den første teknikken er bitpakking. Den består i å bruke nøyaktig det antall biter som er nødvendig for å beskrive ønsket verdi. For eksempel, hvis du har en enum som kan ha 16 forskjellige verdier, kan du bruke bare 8 biter i stedet for en hel byte (4 bits).

Glenn Fiedler forklarer hvordan dette implementeres i andre del av artikkelen Lese- og skrivepakker.

Bitpakking fungerer spesielt godt med prøvetaking, som vil være tema i neste avsnitt.

Prøvetaking

Prøvetaking er en tapsbasert komprimeringsteknikk som bare bruker et undersett av mulige verdier for å kode en verdi. Den enkleste måten å implementere diskretisering på er ved å avrunde flyttall.

Glenn Fiedler (igjen!) viser hvordan man kan sette sampling i praksis i sin artikkel Komprimering av øyeblikksbilder.

Kompresjonsalgoritmer

Den neste teknikken vil være tapsfrie komprimeringsalgoritmer.

Her, etter min mening, er de tre mest interessante algoritmene du trenger å vite:

  • Huffman-koding med forhåndsberegnet kode, som er ekstremt rask og kan gi gode resultater. Den ble brukt til å komprimere pakker i Quake3-nettverksmotoren.
  • zlib er en generell komprimeringsalgoritme som aldri øker datamengden. Hvordan kan du se her, har den blitt brukt i en rekke applikasjoner. Det kan være overflødig for å oppdatere tilstander. Men det kan være nyttig hvis du trenger å sende eiendeler, lange tekster eller terreng til klienter fra serveren.
  • Kopiering av kjørelengder – Dette er nok den enkleste komprimeringsalgoritmen, men den er veldig effektiv for visse typer data, og kan brukes som et forbehandlingstrinn før zlib. Den er spesielt egnet for å komprimere terreng som består av fliser eller voksler der mange tilstøtende elementer gjentas.

Deltakompresjon

Den siste kompresjonsteknikken er deltakompresjon. Den består i det faktum at bare forskjellene mellom gjeldende spilltilstand og den siste tilstanden som mottas av klienten, overføres.

Den ble først brukt i Quake3-nettverksmotoren. Her er to artikler som forklarer hvordan du bruker det:

Glenn Fiedler brukte det også i den andre delen av artikkelen sin Komprimering av øyeblikksbilder.

Kryptering

I tillegg må du kanskje kryptere overføringen av informasjon mellom klienter og serveren. Det er flere grunner til dette:

  • personvern/konfidensialitet: meldinger kan bare leses av mottakeren, og ingen andre personer som snuser på nettverket vil kunne lese dem.
  • autentisering: en person som ønsker å spille rollen som en spiller må kjenne sin nøkkel.
  • Jukseforebygging: Det vil være mye vanskeligere for ondsinnede spillere å lage sine egne juksepakker, de må reprodusere krypteringsskjemaet og finne nøkkelen (som endres med hver tilkobling).

Jeg anbefaler på det sterkeste å bruke et bibliotek til dette. Jeg foreslår å bruke libsodium, fordi det er spesielt enkelt og har utmerkede opplæringsprogrammer. Spesielt interessant er opplæringen om nøkkelutveksling, som lar deg generere nye nøkler med hver nye tilkobling.

Søknadsprotokoll: Konklusjon

Dette avslutter vår søknadsprotokoll. Jeg tror at komprimering er helt valgfritt, og beslutningen om å bruke den avhenger bare av spillet og den nødvendige båndbredden. Kryptering er etter min mening obligatorisk, men i den første prototypen kan du klare deg uten den.

Applikasjonslogikk

Vi er nå i stand til å oppdatere tilstanden i klienten, men kan få problemer med ventetid. Spilleren, etter å ha fullført inndataene, må vente på at spillstatusen oppdateres fra serveren for å se hvilken innvirkning den hadde på verden.

Dessuten, mellom to statlige oppdateringer, er verden fullstendig statisk. Hvis tilstandsoppdateringsraten er lav, vil bevegelsene være veldig rykende.

Det er flere teknikker for å redusere virkningen av dette problemet, og jeg skal dekke dem i neste avsnitt.

Latency smoothing-teknikker

Alle teknikkene som er beskrevet i denne delen er omtalt i detalj i serien Rask flerspiller Gabriel Gambetta. Jeg anbefaler på det sterkeste å lese denne utmerkede artikkelserien. Den inkluderer også en interaktiv demo som lar deg se hvordan disse teknikkene fungerer i praksis.

Den første teknikken er å bruke inndataresultatet direkte uten å vente på svar fra serveren. Det kalles prognoser på klientsiden. Men når klienten mottar en oppdatering fra serveren, må den bekrefte at prediksjonen var korrekt. Hvis dette ikke er tilfelle, trenger han bare å endre tilstanden i henhold til det han mottok fra serveren, fordi serveren er autoritær. Denne teknikken ble først brukt i Quake. Du kan lese mer om det i artikkelen Quake Engine-kodegjennomgang Fabien Sanglars [oversettelse på Habré].

Det andre settet med teknikker brukes til å jevne ut bevegelsen til andre enheter mellom to tilstandsoppdateringer. Det er to måter å løse dette problemet på: interpolasjon og ekstrapolering. Ved interpolering tas de to siste tilstandene og overgangen fra den ene til den andre vises. Ulempen er at det forårsaker en liten forsinkelse fordi klienten alltid ser hva som har skjedd i fortiden. Ekstrapolering handler om å forutsi hvor entiteter skal være nå basert på den siste tilstanden kunden har mottatt. Ulempen er at hvis enheten fullstendig endrer bevegelsesretningen, vil det være en stor feil mellom prognosen og den faktiske posisjonen.

Den siste, mest avanserte teknikken som bare er nyttig i FPS er etterslep kompensasjon. Når du bruker lagkompensasjon, tar serveren hensyn til klientens forsinkelser når den skyter mot målet. For eksempel, hvis en spiller utførte et hodeskudd på skjermen sin, men målet deres var på et annet sted på grunn av forsinkelse, ville det være urettferdig å nekte spilleren retten til å drepe på grunn av forsinkelse. Derfor spoler serveren tiden tilbake til det øyeblikket spilleren skjøt for å simulere det spilleren så på skjermen og se etter kollisjon mellom skuddet og målet.

Glenn Fiedler (som alltid!) skrev en artikkel i 2004 Nettverksfysikk (2004), der han la grunnlaget for synkronisering av fysikksimuleringer mellom server og klient. I 2014 skrev han en ny artikkelserie Nettverksfysikk, som beskrev andre teknikker for synkronisering av fysikksimuleringer.

Det er også to artikler på Valve-wikien, Kilde Multiplayer Networking и Latency-kompenseringsmetoder i klient/server-protokolldesign og -optimalisering i spillet som vurderer kompensasjon for forsinkelser.

Forebygging av juks

Det er to hovedteknikker for å forhindre juks.

For det første: gjør det vanskeligere for juksemakere å sende ondsinnede pakker. Som nevnt ovenfor er kryptering en god måte å implementere dette på.

For det andre: en autoritær server skal bare motta kommandoer/inndata/handlinger. Klienten skal ikke kunne endre tilstand på serveren annet enn ved å sende input. Deretter, hver gang serveren mottar input, må den sjekke om den er gyldig før den brukes.

Anvendelseslogikk: konklusjon

Jeg anbefaler at du implementerer en måte å simulere høye ventetider og lave oppdateringsfrekvenser, slik at du kan teste oppførselen til spillet ditt under dårlige forhold, selv når klienten og serveren kjører på samme datamaskin. Dette vil i stor grad forenkle implementeringen av forsinkelsesutjevningsteknikker.

Andre nyttige ressurser

Hvis du vil utforske andre ressurser på nettverksmodeller, kan du finne dem her:

Kilde: www.habr.com

Legg til en kommentar