Over het netwerkmodel in games voor beginners

Over het netwerkmodel in games voor beginners
De afgelopen twee weken heb ik gewerkt aan de online engine voor mijn game. Voordien wist ik helemaal niets van netwerken in games, dus las ik veel artikelen en deed veel experimenten om alle concepten te begrijpen en mijn eigen netwerkengine te kunnen schrijven.

In deze handleiding wil ik graag de verschillende concepten met u delen die u moet leren voordat u uw eigen game-engine schrijft, evenals de beste bronnen en artikelen om deze te leren.

Over het algemeen zijn er twee hoofdtypen netwerkarchitecturen: peer-to-peer en client-server. In een peer-to-peer (p2p)-architectuur worden gegevens overgedragen tussen elk paar verbonden spelers, terwijl in een client-server-architectuur gegevens alleen worden overgedragen tussen spelers en de server.

Hoewel in sommige games nog steeds peer-to-peer-architectuur wordt gebruikt, is client-server de standaard: het is gemakkelijker te implementeren, vereist een kleinere kanaalbreedte en maakt het gemakkelijker om te beschermen tegen bedrog. Daarom zullen we ons in deze tutorial concentreren op de client-serverarchitectuur.

Wij zijn vooral geïnteresseerd in autoritaire servers: in dergelijke systemen heeft de server altijd gelijk. Als een speler bijvoorbeeld denkt dat hij zich op de coördinaten (10, 5) bevindt, en de serveerder vertelt hem dat hij zich op (5, 3) bevindt, dan moet de cliënt zijn positie vervangen door de positie die door de server wordt gerapporteerd, en niet vice versa. omgekeerd. Het gebruik van gezaghebbende servers maakt het gemakkelijker om valsspelers te identificeren.

Netwerkspelsystemen bestaan ​​uit drie hoofdcomponenten:

  • Transportprotocol: hoe gegevens worden overgedragen tussen clients en server.
  • Applicatieprotocol: wat wordt verzonden van clients naar de server en van de server naar clients en in welk formaat.
  • Applicatielogica: hoe de overgedragen gegevens worden gebruikt om de status van de clients en server bij te werken.

Het is erg belangrijk om de rol van elk onderdeel en de uitdagingen die daarmee gepaard gaan te begrijpen.

Transportprotocol

De eerste stap is het selecteren van een protocol voor het transporteren van gegevens tussen de server en clients. Hiervoor zijn twee internetprotocollen: TCP и UDP. Maar u kunt uw eigen transportprotocol maken op basis van een ervan, of een bibliotheek gebruiken die deze gebruikt.

Vergelijking van TCP en UDP

Zowel TCP als UDP zijn gebaseerd op IP. IP maakt het mogelijk dat een pakket van een bron naar een ontvanger wordt verzonden, maar garandeert niet dat het verzonden pakket vroeg of laat de ontvanger zal bereiken, dat het het minstens één keer zal bereiken en dat de reeks pakketten in de juiste volgorde zal aankomen. volgorde. Bovendien kan een pakket slechts een beperkte hoeveelheid gegevens bevatten, bepaald door de waarde MTU.

UDP is slechts een dunne laag bovenop IP. Daarom heeft het dezelfde beperkingen. TCP heeft daarentegen veel functies. Het biedt een betrouwbare, ordelijke verbinding tussen twee knooppunten met foutcontrole. Daarom is TCP erg handig en wordt het in veel andere protocollen gebruikt, b.v. HTTP, FTP и SMTP. Maar al deze functies hebben een prijs: vertraging.

Om te begrijpen waarom deze functies latentie kunnen veroorzaken, moeten we begrijpen hoe TCP werkt. Wanneer een verzendend knooppunt een pakket naar een ontvangend knooppunt verzendt, verwacht het een bevestiging (ACK) te ontvangen. Als het pakket na een bepaalde tijd niet wordt ontvangen (omdat het pakket of de bevestiging verloren is gegaan of om een ​​andere reden), wordt het pakket opnieuw verzonden. Bovendien garandeert TCP dat pakketten in de juiste volgorde worden ontvangen, zodat alle andere pakketten niet kunnen worden verwerkt totdat het verloren pakket is ontvangen, zelfs als ze al door de ontvangende host zijn ontvangen.

Maar zoals je je waarschijnlijk kunt voorstellen, is latentie in multiplayer-games erg belangrijk, vooral in actievolle genres zoals FPS. Dit is de reden waarom veel games UDP gebruiken met hun eigen protocol.

Een native op UDP gebaseerd protocol kan om verschillende redenen efficiënter zijn dan TCP. Het kan bijvoorbeeld sommige pakketten als vertrouwd en andere als niet-vertrouwd markeren. Daarom maakt het niet uit of het niet-vertrouwde pakket de ontvanger bereikt. Of het kan meerdere datastromen verwerken, zodat een verloren pakket in één stroom de resterende stromen niet vertraagt. Er kan bijvoorbeeld een thread zijn voor spelersinvoer en een andere thread voor chatberichten. Als een chatbericht dat niet urgent is verloren gaat, zal dit de invoer die wel urgent is niet vertragen. Of een eigen protocol kan betrouwbaarheid op een andere manier implementeren dan TCP om efficiënter te zijn in een videogameomgeving.

Dus als TCP zo slecht is, creëren we dan ons eigen transportprotocol op basis van UDP?

Het is iets ingewikkelder. Hoewel TCP bijna niet optimaal is voor gamingnetwerksystemen, kan het voor jouw specifieke game behoorlijk goed werken en je kostbare tijd besparen. Latentie hoeft bijvoorbeeld geen probleem te zijn voor een turn-based game of een game die alleen kan worden gespeeld op LAN-netwerken, waar de latentie en het pakketverlies veel lager zijn dan op internet.

Veel succesvolle games, waaronder World of Warcraft, Minecraft en Terraria, gebruiken TCP. De meeste FPS'en gebruiken echter hun eigen op UDP gebaseerde protocollen, dus daar zullen we hieronder meer over vertellen.

Als u besluit TCP te gebruiken, zorg er dan voor dat dit is uitgeschakeld Het algoritme van Nagle, omdat het pakketten buffert voordat ze worden verzonden, wat betekent dat de latentie toeneemt.

Om meer te leren over de verschillen tussen UDP en TCP in de context van multiplayer-games, kun je het artikel van Glenn Fiedler lezen UDP versus TCP.

Eigen protocol

U wilt dus uw eigen transportprotocol creëren, maar weet niet waar u moet beginnen? Je hebt geluk, want Glenn Fiedler heeft hier twee geweldige artikelen over geschreven. Je zult er veel slimme gedachten in vinden.

Het eerste artikel Netwerken voor gameprogrammeurs 2008, gemakkelijker dan de tweede, Een gamenetwerkprotocol bouwen 2016. Ik raad je aan om met de oudere te beginnen.

Merk op dat Glenn Fiedler een groot voorstander is van het gebruik van een aangepast protocol gebaseerd op UDP. En na het lezen van zijn artikelen zul je waarschijnlijk zijn mening overnemen dat TCP ernstige tekortkomingen heeft in videogames, en zul je je eigen protocol willen implementeren.

Maar als je nieuw bent op het gebied van netwerken, doe jezelf dan een plezier en gebruik TCP of een bibliotheek. Om uw eigen transportprotocol succesvol te implementeren, moet u vooraf veel leren.

Netwerkbibliotheken

Als je iets efficiënter nodig hebt dan TCP, maar geen zin hebt in de rompslomp van het implementeren van je eigen protocol en het ingaan op veel details, dan kun je een netwerkbibliotheek gebruiken. Er zijn er veel:

Ik heb ze niet allemaal geprobeerd, maar ik geef de voorkeur aan ENet omdat het gemakkelijk te gebruiken en betrouwbaar is. Daarnaast bevat het duidelijke documentatie en een tutorial voor beginners.

Transportprotocol: conclusie

Samenvattend: er zijn twee belangrijke transportprotocollen: TCP en UDP. TCP heeft veel handige functies: betrouwbaarheid, behoud van pakketvolgorde, foutdetectie. UDP heeft dit allemaal niet, maar TCP heeft van nature een verhoogde latentie, wat voor sommige games onaanvaardbaar is. Dat wil zeggen, om een ​​lage latentie te garanderen, kunt u uw eigen protocol maken op basis van UDP of een bibliotheek gebruiken die een transportprotocol op UDP implementeert en is aangepast voor videogames voor meerdere spelers.

De keuze tussen TCP, UDP en de bibliotheek hangt van verschillende factoren af. Ten eerste, vanuit de behoeften van het spel: heeft het een lage latentie nodig? Ten tweede, wat betreft de vereisten voor het applicatieprotocol: heeft het een betrouwbaar protocol nodig? Zoals we in het volgende deel zullen zien, is het mogelijk een applicatieprotocol te maken waarvoor een niet-vertrouwd protocol zeer geschikt is. Ten slotte moet u ook rekening houden met de ervaring van de netwerkengine-ontwikkelaar.

Ik heb twee adviezen:

  • Abstraheer het transportprotocol zoveel mogelijk van de rest van de applicatie, zodat het eenvoudig kan worden vervangen zonder de hele code te herschrijven.
  • Overoptimaliseer niet. Als u geen netwerkexpert bent en niet zeker weet of u een aangepast, op UDP gebaseerd transportprotocol nodig heeft, kunt u beginnen met TCP of een bibliotheek die betrouwbaarheid biedt, en vervolgens de prestaties testen en meten. Als er zich problemen voordoen en u er zeker van bent dat de oorzaak bij het transportprotocol ligt, is het wellicht tijd om uw eigen transportprotocol te maken.

Aan het einde van dit deel raad ik u aan dit te lezen Inleiding tot het programmeren van multiplayer-games door Brian Hook, waarin veel van de hier besproken onderwerpen worden behandeld.

Applicatieprotocol

Nu we gegevens kunnen uitwisselen tussen clients en server, moeten we beslissen welke gegevens we willen overbrengen en in welk formaat.

Het klassieke schema is dat clients invoer of acties naar de server sturen, en dat de server de huidige spelstatus naar de clients stuurt.

De server verzendt niet de volledige status, maar een gefilterde status met entiteiten die zich in de buurt van de speler bevinden. Hij doet dit om drie redenen. Ten eerste kan de volledige toestand te groot zijn om met hoge frequentie te worden verzonden. Ten tweede zijn klanten vooral geïnteresseerd in visuele en audiogegevens, omdat de meeste spellogica op de spelserver wordt gesimuleerd. Ten derde hoeft de speler in sommige games bepaalde gegevens niet te kennen, bijvoorbeeld de positie van de vijand aan de andere kant van de kaart, anders kan hij pakketten opsnuiven en precies weten waar hij heen moet om hem te doden.

Serialisatie

De eerste stap is het converteren van de gegevens die we willen verzenden (invoer- of gamestatus) naar een formaat dat geschikt is voor verzending. Dit proces wordt genoemd serialisatie.

De gedachte die meteen in je opkomt is het gebruik van een voor mensen leesbaar formaat, zoals JSON of XML. Maar dit zal totaal ineffectief zijn en het grootste deel van het kanaal verspillen.

Het wordt aanbevolen om in plaats daarvan het binaire formaat te gebruiken, dat veel compacter is. Dat wil zeggen dat de pakketten slechts enkele bytes zullen bevatten. Er is hier een probleem dat we moeten overwegen bytevolgorde, die op verschillende computers kan verschillen.

Om gegevens te serialiseren, kunt u bijvoorbeeld een bibliotheek gebruiken:

Zorg er wel voor dat de bibliotheek draagbare archieven aanmaakt en zich bekommert om endianness.

Een alternatieve oplossing is om het zelf te implementeren; het is niet bijzonder moeilijk, vooral als u een datacentrische benadering van uw code gebruikt. Bovendien kunt u optimalisaties uitvoeren die niet altijd mogelijk zijn bij het gebruik van de bibliotheek.

Glenn Fiedler schreef twee artikelen over serialisatie: Pakketten lezen en schrijven и Serialisatiestrategieën.

samendrukking

De hoeveelheid gegevens die tussen clients en server wordt overgedragen, wordt beperkt door de bandbreedte van het kanaal. Met datacompressie kunt u meer gegevens in elke snapshot overbrengen, de updatefrequentie verhogen of eenvoudigweg de kanaalvereisten verlagen.

Beetje verpakking

De eerste techniek is het inpakken van bits. Het bestaat uit het gebruik van precies het aantal bits dat nodig is om de gewenste waarde te beschrijven. Als u bijvoorbeeld een opsomming heeft die 16 verschillende waarden kan hebben, kunt u in plaats van een hele byte (8 bits) slechts 4 bits gebruiken.

Glenn Fiedler legt in het tweede deel van het artikel uit hoe je dit kunt implementeren Pakketten lezen en schrijven.

Bitpacking werkt vooral goed bij sampling, wat het onderwerp van de volgende sectie zal zijn.

Bemonstering

Bemonstering is een compressietechniek met verlies die slechts een subset van mogelijke waarden gebruikt om een ​​waarde te coderen. De eenvoudigste manier om discretisatie te implementeren is door drijvende-kommagetallen af ​​te ronden.

Glenn Fiedler laat in zijn artikel (opnieuw!) zien hoe je sampling in de praktijk kunt brengen Snapshot-compressie.

Compressie-algoritmen

De volgende techniek zijn verliesvrije compressie-algoritmen.

Hier zijn naar mijn mening de drie meest interessante algoritmen die je moet kennen:

  • Huffman-codering met vooraf berekende code, die extreem snel is en goede resultaten kan opleveren. Het werd gebruikt om pakketten te comprimeren in de Quake3-netwerkengine.
  • zlib is een compressie-algoritme voor algemene doeleinden dat de hoeveelheid gegevens nooit vergroot. Hoe kun je zien hier, het is in verschillende toepassingen gebruikt. Het kan overbodig zijn voor het bijwerken van statussen. Maar het kan handig zijn als u vanaf de server assets, lange teksten of terrein naar clients moet sturen.
  • Runlengtes kopiëren - Dit is waarschijnlijk het eenvoudigste compressiealgoritme, maar het is zeer effectief voor bepaalde soorten gegevens en kan worden gebruikt als voorverwerkingsstap vóór zlib. Het is met name geschikt voor het comprimeren van terrein dat bestaat uit tegels of voxels waarin veel aangrenzende elementen worden herhaald.

Delta-compressie

De laatste compressietechniek is deltacompressie. Het bestaat uit het feit dat alleen de verschillen tussen de huidige spelstatus en de laatste door de klant ontvangen status worden verzonden.

Het werd voor het eerst gebruikt in de Quake3-netwerkengine. Hier zijn twee artikelen waarin wordt uitgelegd hoe u het kunt gebruiken:

Glenn Fiedler gebruikte het ook in het tweede deel van zijn artikel Snapshot-compressie.

Versleuteling

Bovendien moet u mogelijk de gegevensoverdracht tussen clients en de server coderen. Hier zijn verschillende redenen voor:

  • privacy/vertrouwelijkheid: berichten kunnen alleen door de ontvanger worden gelezen en geen enkele andere persoon die aan het netwerk snuffelt, kan ze lezen.
  • authenticatie: iemand die de rol van speler wil spelen, moet zijn sleutel kennen.
  • Preventie van cheats: Het zal voor kwaadwillende spelers veel moeilijker zijn om hun eigen cheatpakketten te maken; ze zullen het coderingsschema moeten reproduceren en de sleutel moeten vinden (die bij elke verbinding verandert).

Ik raad ten zeerste aan hiervoor een bibliotheek te gebruiken. Ik stel voor om te gebruiken libnatrium, omdat het bijzonder eenvoudig is en uitstekende tutorials heeft. Bijzonder interessant is de tutorial over sleutel uitwisseling, waarmee u bij elke nieuwe verbinding nieuwe sleutels kunt genereren.

Toepassingsprotocol: conclusie

Hiermee is ons toepassingsprotocol voltooid. Ik geloof dat compressie volledig optioneel is en dat de beslissing om het te gebruiken alleen afhangt van het spel en de vereiste bandbreedte. Versleuteling is naar mijn mening verplicht, maar in het eerste prototype kun je het zonder doen.

Applicatielogica

We kunnen nu de status in de client bijwerken, maar er kunnen latentieproblemen optreden. De speler moet, na het voltooien van de invoer, wachten tot de spelstatus vanaf de server is bijgewerkt om te zien welke impact deze op de wereld heeft gehad.

Bovendien is de wereld tussen twee statusupdates volledig statisch. Als de statusupdatesnelheid laag is, zullen de bewegingen erg schokkerig zijn.

Er zijn verschillende technieken om de impact van dit probleem te verminderen, en ik zal ze in de volgende sectie bespreken.

Latency Smoothing-technieken

Alle technieken die in deze sectie worden beschreven, worden in de serie gedetailleerd besproken Snelle multiplayer Gabriël Gambetta. Ik raad u ten zeerste aan deze uitstekende reeks artikelen te lezen. Er zit ook een interactieve demo bij waarmee je kunt zien hoe deze technieken in de praktijk werken.

De eerste techniek is om het invoerresultaat direct toe te passen zonder te wachten op een reactie van de server. Het heet Prognoses aan de klantzijde. Wanneer de client echter een update van de server ontvangt, moet deze verifiëren of de voorspelling correct was. Als dit niet het geval is, hoeft hij alleen maar zijn toestand te veranderen op basis van wat hij van de server heeft ontvangen, omdat de server autoritair is. Deze techniek werd voor het eerst gebruikt in Quake. Je kunt er meer over lezen in het artikel Quake Engine-codebeoordeling Fabien Sanglars [vertaling op Habré].

De tweede reeks technieken wordt gebruikt om de beweging van andere entiteiten tussen twee statusupdates te vergemakkelijken. Er zijn twee manieren om dit probleem op te lossen: interpolatie en extrapolatie. Bij interpolatie worden de laatste twee toestanden genomen en wordt de overgang van de ene naar de andere weergegeven. Het nadeel is dat het een kleine vertraging veroorzaakt, omdat de klant altijd ziet wat er in het verleden is gebeurd. Bij extrapolatie gaat het om het voorspellen waar entiteiten zich nu zouden moeten bevinden op basis van de laatste status die de klant heeft ontvangen. Het nadeel is dat als de entiteit de bewegingsrichting volledig verandert, er een grote fout zal zijn tussen de voorspelling en de werkelijke positie.

De nieuwste, meest geavanceerde techniek die alleen nuttig is in FPS is vertragingscompensatie. Bij het gebruik van lagcompensatie houdt de server rekening met de vertragingen van de client wanneer deze op het doel schiet. Als een speler bijvoorbeeld een headshot op zijn scherm uitvoert, maar zijn doelwit zich in werkelijkheid vanwege vertraging op een andere locatie bevindt, dan zou het oneerlijk zijn om de speler het recht te ontzeggen om te doden vanwege vertraging. Daarom spoelt de server de tijd terug naar het moment waarop de speler schoot om te simuleren wat de speler op zijn scherm zag en om te controleren op een botsing tussen zijn schot en het doel.

Glenn Fiedler schreef (zoals altijd!) een artikel in 2004 Netwerkfysica (2004), waarin hij de basis legde voor het synchroniseren van natuurkundige simulaties tussen server en client. In 2014 schreef hij een nieuwe serie artikelen Netwerkfysica, waarin andere technieken werden beschreven voor het synchroniseren van natuurkundige simulaties.

Er zijn ook twee artikelen op de Valve-wiki, Bron Multiplayer-netwerken и Latentiecompensatiemethoden bij het ontwerpen en optimaliseren van in-game protocollen voor clients/servers die rekening houden met compensatie voor vertragingen.

Het voorkomen van bedrog

Er zijn twee belangrijke technieken om bedrog te voorkomen.

Ten eerste: het voor valsspelers moeilijker maken om kwaadaardige pakketten te verzenden. Zoals hierboven vermeld, is encryptie een goede manier om dit te implementeren.

Ten tweede: een autoritaire server mag alleen opdrachten/invoer/acties ontvangen. De client mag de status op de server alleen kunnen wijzigen door invoer te verzenden. Elke keer dat de server invoer ontvangt, moet hij vervolgens controleren of deze geldig is voordat hij deze gebruikt.

Toepassingslogica: conclusie

Ik raad je aan een manier te implementeren om hoge latenties en lage vernieuwingsfrequenties te simuleren, zodat je het gedrag van je game onder slechte omstandigheden kunt testen, zelfs als de client en de server op dezelfde computer draaien. Dit zal de implementatie van vertragingsafvlakkingstechnieken aanzienlijk vereenvoudigen.

Andere nuttige bronnen

Als u andere bronnen over netwerkmodellen wilt verkennen, kunt u deze hier vinden:

Bron: www.habr.com

Voeg een reactie