Om nätverksmodellen i spel för nybörjare

Om nätverksmodellen i spel för nybörjare
De senaste två veckorna har jag arbetat med onlinemotorn för mitt spel. Innan detta visste jag ingenting alls om nätverkande i spel, så jag läste många artiklar och gjorde många experiment för att förstå alla begrepp och kunna skriva min egen nätverksmotor.

I den här guiden vill jag dela med dig av de olika begreppen du behöver lära dig innan du skriver din egen spelmotor, samt de bästa resurserna och artiklarna för att lära dig dem.

I allmänhet finns det två huvudtyper av nätverksarkitekturer: peer-to-peer och klient-server. I en peer-to-peer (p2p)-arkitektur överförs data mellan valfria par av anslutna spelare, medan i en klient-server-arkitektur överförs data endast mellan spelare och servern.

Även om peer-to-peer-arkitektur fortfarande används i vissa spel, är klient-server standarden: den är lättare att implementera, kräver en mindre kanalbredd och gör det lättare att skydda mot fusk. Därför kommer vi i denna handledning att fokusera på klient-server-arkitekturen.

I synnerhet är vi mest intresserade av auktoritära servrar: i sådana system har servern alltid rätt. Till exempel, om en spelare tror att han är på koordinaterna (10, 5) och servern säger till honom att han är på (5, 3), så ska klienten ersätta sin position med den som rapporterats av servern, och inte vice versa. Att använda auktoritativa servrar gör det lättare att identifiera fuskare.

Nätverksspelsystem har tre huvudkomponenter:

  • Transportprotokoll: hur data överförs mellan klienter och server.
  • Tillämpningsprotokoll: vad som överförs från klienter till servern och från servern till klienter och i vilket format.
  • Applikationslogik: hur överförda data används för att uppdatera tillståndet för klienterna och servern.

Det är mycket viktigt att förstå varje dels roll och de utmaningar som är förknippade med dem.

Transportprotokoll

Det första steget är att välja ett protokoll för att transportera data mellan servern och klienterna. Det finns två internetprotokoll för detta: TCP и UDP. Men du kan skapa ditt eget transportprotokoll baserat på ett av dem eller använda ett bibliotek som använder dem.

Jämförelse av TCP och UDP

Både TCP och UDP är baserade på IP. IP tillåter att ett paket överförs från en källa till en mottagare, men garanterar inte att det skickade paketet förr eller senare kommer att nå mottagaren, att det når det minst en gång och att paketsekvensen kommer fram i rätt beställa. Dessutom kan ett paket endast innehålla en begränsad mängd data, givet av värdet MTU.

UDP är bara ett tunt lager ovanpå IP. Därför har den samma begränsningar. TCP har däremot många funktioner. Det ger en pålitlig, ordnad anslutning mellan två noder med felkontroll. Därför är TCP mycket bekvämt och används i många andra protokoll, t.ex. HTTP, FTP и SMTP-. Men alla dessa funktioner har ett pris: dröjsmål.

För att förstå varför dessa funktioner kan orsaka latens måste vi förstå hur TCP fungerar. När en sändande nod sänder ett paket till en mottagande nod, förväntar den sig att ta emot en bekräftelse (ACK). Om den inte tar emot det efter en viss tid (på grund av att paketet eller bekräftelsen har förlorats, eller av någon annan anledning), så skickar den om paketet. Dessutom garanterar TCP att paket tas emot i rätt ordning, så tills det förlorade paketet tas emot kan alla andra paket inte behandlas, även om de redan har tagits emot av den mottagande värden.

Men som du säkert kan föreställa dig är latens i multiplayer-spel väldigt viktigt, speciellt i actionfyllda genrer som FPS. Det är därför många spel använder UDP med sitt eget protokoll.

Ett inbyggt UDP-baserat protokoll kan vara mer effektivt än TCP av olika anledningar. Det kan till exempel markera vissa paket som betrodda och andra som otillförlitliga. Därför bryr det sig inte om det opålitliga paketet når mottagaren. Eller så kan den bearbeta flera dataströmmar så att ett förlorat paket i en ström inte saktar ner de återstående strömmarna. Till exempel kan det finnas en tråd för spelarinput och en annan tråd för chattmeddelanden. Om ett chattmeddelande som inte är brådskande försvinner, kommer det inte att sakta ner inmatning som är brådskande. Eller ett proprietärt protokoll kan implementera tillförlitlighet annorlunda än TCP för att vara mer effektivt i en videospelsmiljö.

Så om TCP suger så mycket, då skapar vi vårt eget transportprotokoll baserat på UDP?

Det är lite mer komplicerat. Även om TCP är nästan suboptimalt för spelnätverkssystem, kan det fungera ganska bra för ditt specifika spel och spara värdefull tid. Till exempel kan latens inte vara ett problem för ett turbaserat spel eller ett spel som bara kan spelas på LAN-nätverk, där latens och paketförlust är mycket lägre än på Internet.

Många framgångsrika spel, inklusive World of Warcraft, Minecraft och Terraria, använder TCP. De flesta FPS använder dock sina egna UDP-baserade protokoll, så vi kommer att prata mer om dem nedan.

Om du bestämmer dig för att använda TCP, se till att det är inaktiverat Nagles algoritm, eftersom det buffrar paket innan de skickas, vilket betyder att det ökar latensen.

För att lära dig mer om skillnaderna mellan UDP och TCP i samband med multiplayer-spel kan du läsa Glenn Fiedlers artikel UDP vs. TCP.

Eget protokoll

Så du vill skapa ditt eget transportprotokoll, men vet inte var du ska börja? Du har tur eftersom Glenn Fiedler har skrivit två fantastiska artiklar om detta. Du hittar många smarta tankar i dem.

Den första artikeln Nätverk för spelprogrammerare 2008, lättare än den andra, Bygga ett spelnätverksprotokoll 2016. Jag rekommenderar att du börjar med den äldre.

Observera att Glenn Fiedler är en stor förespråkare för att använda ett anpassat protokoll baserat på UDP. Och efter att ha läst hans artiklar kommer du förmodligen att anta hans åsikt att TCP har allvarliga brister i videospel, och du kommer att vilja implementera ditt eget protokoll.

Men om du är ny på nätverkande, gör dig själv en tjänst och använd TCP eller ett bibliotek. För att framgångsrikt implementera ditt eget transportprotokoll måste du lära dig mycket i förväg.

Nätverksbibliotek

Om du behöver något mer effektivt än TCP, men inte vill gå igenom besväret med att implementera ditt eget protokoll och gå in på mycket detaljer, kan du använda ett nätverksbibliotek. Det finns många av dem:

Jag har inte provat dem alla, men jag föredrar ENet eftersom det är lätt att använda och pålitligt. Dessutom har den tydlig dokumentation och en handledning för nybörjare.

Transportprotokoll: Slutsats

För att sammanfatta: det finns två huvudsakliga transportprotokoll: TCP och UDP. TCP har många användbara funktioner: tillförlitlighet, paketordningsbevarande, feldetektering. UDP har inte allt detta, men TCP har till sin natur ökad latens, vilket är oacceptabelt för vissa spel. Det vill säga, för att säkerställa låg latens kan du skapa ditt eget protokoll baserat på UDP eller använda ett bibliotek som implementerar ett transportprotokoll på UDP och är anpassat för multiplayer-videospel.

Valet mellan TCP, UDP och biblioteket beror på flera faktorer. För det första, från spelets behov: behöver det låg latens? För det andra, från applikationsprotokollkraven: behöver det ett tillförlitligt protokoll? Som vi kommer att se i nästa del är det möjligt att skapa ett applikationsprotokoll för vilket ett opålitligt protokoll är ganska lämpligt. Slutligen måste du också ta hänsyn till erfarenheten av nätverksmotorutvecklaren.

Jag har två råd:

  • Abstrahera transportprotokollet från resten av applikationen så mycket som möjligt så att det enkelt kan ersättas utan att skriva om all kod.
  • Överoptimera inte. Om du inte är en nätverksexpert och inte är säker på om du behöver ett anpassat UDP-baserat transportprotokoll kan du börja med TCP eller ett bibliotek som ger tillförlitlighet och sedan testa och mäta prestanda. Om problem uppstår och du är säker på att orsaken är transportprotokollet, kan det vara dags att skapa ditt eget transportprotokoll.

I slutet av denna del rekommenderar jag att du läser Introduktion till multiplayer spelprogrammering av Brian Hook, som täcker många av de ämnen som diskuteras här.

Applikationsprotokoll

Nu när vi kan utbyta data mellan klienter och server måste vi bestämma vilken data som ska överföras och i vilket format.

Det klassiska schemat är att klienter skickar input eller åtgärder till servern, och servern skickar det aktuella spelläget till klienterna.

Servern skickar inte hela tillståndet, utan ett filtrerat tillstånd med enheter som finns nära spelaren. Han gör detta av tre anledningar. För det första kan det fullständiga tillståndet vara för stort för att sändas med hög frekvens. För det andra är klienter främst intresserade av bild- och ljuddata, eftersom det mesta av spellogiken simuleras på spelservern. För det tredje, i vissa spel behöver spelaren inte känna till viss data, till exempel fiendens position på andra sidan kartan, annars kan han sniffa paket och veta exakt var han ska flytta för att döda honom.

Serialisering

Det första steget är att konvertera den data vi vill skicka (indata eller spelstatus) till ett format som är lämpligt för överföring. Denna process kallas serialisering.

Tanken som omedelbart kommer att tänka på är att använda ett läsbart format som JSON eller XML. Men detta kommer att vara helt ineffektivt och kommer att slösa bort det mesta av kanalen.

Det rekommenderas att istället använda det binära formatet, som är mycket mer kompakt. Det vill säga, paketen kommer bara att innehålla ett fåtal byte. Det finns ett problem att ta hänsyn till här byte-ordning, som kan skilja sig åt på olika datorer.

För att serialisera data kan du använda ett bibliotek, till exempel:

Se bara till att biblioteket skapar bärbara arkiv och bryr sig om endianness.

En alternativ lösning är att implementera det själv, det är inte särskilt svårt, särskilt om du använder en datacentrerad strategi för din kod. Dessutom kommer det att tillåta dig att utföra optimeringar som inte alltid är möjliga när du använder biblioteket.

Glenn Fiedler skrev två artiklar om serialisering: Läsa och skriva paket и Serialiseringsstrategier.

kompression

Mängden data som överförs mellan klienter och server begränsas av kanalens bandbredd. Datakomprimering gör att du kan överföra mer data i varje ögonblicksbild, öka uppdateringsfrekvensen eller helt enkelt minska kanalkraven.

Bitsförpackning

Den första tekniken är bitpackning. Den består av att använda exakt det antal bitar som krävs för att beskriva det önskade värdet. Till exempel, om du har en enum som kan ha 16 olika värden, kan du istället för en hel byte (8 bitar) använda bara 4 bitar.

Glenn Fiedler förklarar hur man implementerar detta i den andra delen av artikeln Läsa och skriva paket.

Bitpackning fungerar särskilt bra med sampling, vilket kommer att vara ämnet för nästa avsnitt.

Provtagning

Provtagning är en förlustkompressionsteknik som endast använder en delmängd av möjliga värden för att koda ett värde. Det enklaste sättet att implementera diskretisering är genom att avrunda flyttal.

Glenn Fiedler (igen!) visar hur man omsätter sampling i praktiken i sin artikel Snapshot-komprimering.

Kompressionsalgoritmer

Nästa teknik kommer att vara förlustfria komprimeringsalgoritmer.

Här, enligt min mening, är de tre mest intressanta algoritmerna du behöver känna till:

  • Huffman kodning med förberäknad kod, som är extremt snabb och kan ge bra resultat. Den användes för att komprimera paket i nätverksmotorn Quake3.
  • zlib är en allmän komprimeringsalgoritm som aldrig ökar mängden data. Hur kan du se härhar den använts i en mängd olika applikationer. Det kan vara överflödigt för att uppdatera tillstånd. Men det kan vara användbart om du behöver skicka tillgångar, långa texter eller terräng till klienter från servern.
  • Kopiera körlängder – Det här är förmodligen den enklaste komprimeringsalgoritmen, men den är väldigt effektiv för vissa typer av data, och kan användas som ett förbearbetningssteg före zlib. Den är särskilt lämplig för att komprimera terräng som består av plattor eller voxels där många intilliggande element upprepas.

Deltakompression

Den sista kompressionstekniken är deltakompression. Den består i det faktum att endast skillnaderna mellan det aktuella speltillståndet och det senaste tillståndet som mottas av klienten överförs.

Den användes först i nätverksmotorn Quake3. Här är två artiklar som förklarar hur du använder det:

Glenn Fiedler använde det också i den andra delen av sin artikel Snapshot-komprimering.

Шифрование

Dessutom kan du behöva kryptera överföringen av information mellan klienter och servern. Det finns flera anledningar till detta:

  • sekretess/sekretess: meddelanden kan bara läsas av mottagaren, och ingen annan person som sniffar nätverket kommer att kunna läsa dem.
  • autentisering: en person som vill spela rollen som en spelare måste känna till sin nyckel.
  • Fuskförebyggande: Det kommer att bli mycket svårare för illvilliga spelare att skapa sina egna fuskpaket, de måste reproducera krypteringsschemat och hitta nyckeln (som ändras med varje anslutning).

Jag rekommenderar starkt att du använder ett bibliotek för detta. Jag föreslår att du använder libsodium, eftersom det är särskilt enkelt och har utmärkta tutorials. Särskilt intressant är handledningen om nyckelbyte, som låter dig generera nya nycklar med varje ny anslutning.

Ansökningsprotokoll: Slutsats

Detta avslutar vårt ansökningsprotokoll. Jag tror att komprimering är helt valfritt och beslutet att använda det beror bara på spelet och den nödvändiga bandbredden. Kryptering är enligt min mening obligatorisk, men i den första prototypen kan du klara dig utan den.

Applikationslogik

Vi kan nu uppdatera tillståndet i klienten, men kan stöta på latensproblem. Spelaren måste, efter att ha slutfört inmatningen, vänta på att speltillståndet uppdateras från servern för att se vilken inverkan det hade på världen.

Dessutom, mellan två statliga uppdateringar, är världen helt statisk. Om tillståndsuppdateringshastigheten är låg kommer rörelserna att bli mycket ryckiga.

Det finns flera tekniker för att minska effekten av detta problem, och jag kommer att täcka dem i nästa avsnitt.

Tekniker för utjämning av latens

Alla tekniker som beskrivs i detta avsnitt diskuteras i detalj i serien Snabbt multiplayer Gabriel Gambetta. Jag rekommenderar starkt att läsa denna utmärkta artikelserie. Den innehåller också en interaktiv demo som låter dig se hur dessa tekniker fungerar i praktiken.

Den första tekniken är att applicera inmatningsresultatet direkt utan att vänta på ett svar från servern. Det kallas prognoser på klientsidan. Men när klienten får en uppdatering från servern måste den verifiera att dess förutsägelse var korrekt. Om detta inte är fallet behöver han bara ändra sitt tillstånd enligt vad han fick från servern, eftersom servern är auktoritär. Denna teknik användes först i Quake. Du kan läsa mer om det i artikeln Quake Engine-kodgranskning Fabien Sanglars [översättning på Habré].

Den andra uppsättningen tekniker används för att jämna ut rörelsen för andra enheter mellan två tillståndsuppdateringar. Det finns två sätt att lösa detta problem: interpolation och extrapolation. Vid interpolation tas de två sista tillstånden och övergången från det ena till det andra visas. Dess nackdel är att det orsakar en liten mängd förseningar eftersom klienten alltid ser vad som hänt tidigare. Extrapolering handlar om att förutsäga var entiteter nu ska vara baserat på det senaste tillståndet som kunden tog emot. Dess nackdel är att om enheten helt ändrar rörelseriktningen, kommer det att finnas ett stort fel mellan prognosen och den faktiska positionen.

Den senaste, mest avancerade tekniken som bara är användbar i FPS är eftersläpningsersättning. När man använder fördröjningskompensation tar servern hänsyn till klientens förseningar när den skjuter mot målet. Till exempel, om en spelare gjorde ett headshot på sin skärm, men i verkligheten var deras mål på en annan plats på grund av försening, då skulle det vara orättvist att neka spelaren rätten att döda på grund av försening. Därför spolar servern tillbaka tiden till det ögonblick då spelaren sköt för att simulera vad spelaren såg på sin skärm och kolla efter kollision mellan deras skott och målet.

Glenn Fiedler (som alltid!) skrev en artikel 2004 Nätverksfysik (2004), där han lade grunden för att synkronisera fysiksimuleringar mellan server och klient. 2014 skrev han en ny artikelserie Nätverksfysik, som beskrev andra tekniker för att synkronisera fysiksimuleringar.

Det finns också två artiklar på Valve-wikin, Källa Multiplayer Networking и Latenskompensationsmetoder i klient/server Protokolldesign och optimering i spelet som överväger ersättning för förseningar.

Förhindra fusk

Det finns två huvudtekniker för att förhindra fusk.

För det första: gör det svårare för fuskare att skicka skadliga paket. Som nämnts ovan är ett bra sätt att implementera detta kryptering.

För det andra: en auktoritär server ska bara ta emot kommandon/indata/åtgärder. Klienten ska inte kunna ändra tillstånd på servern annat än genom att skicka indata. Sedan, varje gång servern tar emot indata, måste den kontrollera om den är giltig innan den används.

Tillämpningslogik: slutsats

Jag rekommenderar att du implementerar ett sätt att simulera höga latenser och låga uppdateringsfrekvenser så att du kan testa ditt spels beteende under dåliga förhållanden, även när klienten och servern körs på samma dator. Detta kommer att avsevärt förenkla implementeringen av fördröjningsutjämningstekniker.

Andra användbara resurser

Om du vill utforska andra resurser om nätverksmodeller kan du hitta dem här:

Källa: will.com

Lägg en kommentar