
De afgelopen twee weken heb ik aan een netwerkmotor voor mijn spel gewerkt. Voorheen wist ik helemaal niets over netwerktechnologieƫn in games, dus heb ik veel artikelen gelezen en talloze experimenten gedaan om alle concepten te begrijpen en in staat te zijn om mijn eigen netwerkmotor te schrijven.
In deze handleiding wil ik verschillende concepten met je delen die je moet bestuderen voordat je je eigen game-engine schrijft, evenals de beste bronnen en artikelen om dat te leren.
Over het algemeen zijn er twee hoofdtypen netwerkarchitecturen: peer-to-peer en client-server. In de peer-to-peer (p2p) architectuur worden gegevens tussen willekeurige paren van aangesloten spelers verzonden, terwijl in de client-serverarchitectuur gegevens alleen tussen spelers en de server worden verzonden.
Hoewel de peer-to-peer architectuur nog steeds in sommige spellen wordt gebruikt, is de client-serverarchitectuur de standaard: deze is gemakkelijker te implementeren, vereist minder bandbreedte en vergemakkelijkt de bescherming tegen valsspelen. Daarom zullen we ons in deze handleiding richten op de client-serverarchitectuur.
In het bijzonder zijn autoritaire servers voor ons het meest interessant: in dergelijke systemen heeft de server altijd gelijk. Als een speler denkt dat hij zich op de coƶrdinaten (10, 5) bevindt, en de server zegt dat hij zich op (5, 3) bevindt, moet de client zijn positie vervangen door die van de server en niet andersom. Het gebruik van autoritaire servers vereenvoudigt het herkennen van valsspelers.
In game-netwerksystemen zijn er drie hoofdcomponenten:
- Transportprotocol: hoe worden gegevens tussen clients en de server verzonden.
- Toepassingsprotocol: wat wordt van clients naar de server en van de server naar clients verzonden en in welk formaat.
- Applicatielogica: hoe de verzonden gegevens worden gebruikt om de status van clients en de server bij te werken.
Het is erg belangrijk om de rol van elk onderdeel en de bijbehorende moeilijkheden te begrijpen.
Transportprotocol
De eerste stap is het kiezen van een protocol voor het transporteren van gegevens tussen de server en clients. Hiervoor zijn er twee internetprotocollen: en . Maar je kunt ook je eigen transportprotocol maken op basis van een van deze of een bibliotheek gebruiken waarin ze worden toegepast.
Vergelijking TCP en UDP
Zowel TCP als UDP zijn gebaseerd op Een IP-pakket kan worden verzonden van de verzender naar de ontvanger, maar er zijn geen garanties dat het verzonden pakket ooit bij de ontvanger aankomt, dat het in ieder geval eenmaal zal aankomen en dat de volgorde van de pakketten correct zal zijn. Bovendien kan een pakket maar een beperkte hoeveelheid gegevens bevatten, wat wordt bepaald door de grootte. .
UDP is slechts een dunne laag bovenop IP. Het heeft dus dezelfde beperkingen. In tegenstelling tot UDP heeft TCP veel kenmerken. Het biedt een betrouwbare, geordende verbinding tussen twee knooppunten met foutcontrole. Daarom is TCP zeer handig en wordt het in veel andere protocollen gebruikt, bijvoorbeeld in , en . Maar al deze functies hebben hun prijs: .
Om te begrijpen waarom deze functies vertraging kunnen veroorzaken, moeten we kijken naar hoe TCP werkt. Wanneer het verzendende knooppunt een pakket naar het ontvangende knooppunt verzendt, wacht het op een bevestiging (ACK). Als het na een bepaalde tijd geen bevestiging ontvangt (omdat het pakket of de bevestiging verloren is gegaan, of om andere redenen), wordt het pakket opnieuw verzonden. Bovendien garandeert TCP dat de pakketten in de juiste volgorde worden ontvangen, dus zolang het verloren pakket niet is ontvangen, kunnen alle andere pakketten niet worden verwerkt, zelfs als ze al zijn ontvangen door het ontvangende knooppunt.
Maar zoals je waarschijnlijk begrijpt, is vertraging in multiplayergames heel belangrijk, vooral in actieve genres zoals FPS. Daarom gebruiken veel games UDP met een eigen protocol.
Een eigen protocol op basis van UDP kan om verschillende redenen efficiƫnter zijn dan TCP. Bijvoorbeeld, het kan sommige pakketten als betrouwbaar markeren en andere als onbetrouwbaar. Daarom maakt het zich geen zorgen over de vraag of een onbetrouwbaar pakket de ontvanger heeft bereikt. Of het kan meerdere datastromen verwerken, zodat een verloren pakket in ƩƩn stroom de andere stromen niet vertraagt. Bijvoorbeeld, er kan een stroom voor de invoer van de speler zijn en een andere stroom voor chatberichten. Als een chatbericht, dat geen urgente gegevens bevat, verloren gaat, vertraagt dit de verwerking van de invoer, die urgent is niet.
Dus, als TCP zo slecht is, gaan we dan onze eigen transportprotocol op basis van UDP maken?
Het is iets complexer. Ook al is TCP bijna suboptimaal voor game-netwerksystemen, het kan heel goed functioneren in jouw specifieke game en je kostbare tijd besparen. Bijvoorbeeld, latency is misschien geen probleem voor een turn-based game of een game die alleen in LAN-netwerken kan worden gespeeld, waar latencies en pakketverlies veel minder zijn dan op het internet.
In veel succesvolle games, waaronder World of Warcraft, Minecraft en Terraria, wordt TCP gebruikt. Echter, in de meeste FPS worden eigen protocollen op basis van UDP toegepast, dus hieronder zullen we daar uitgebreider op ingaan.
Als je besluit TCP te gebruiken, zorg er dan voor dat uitgeschakeld is, omdat het pakketten buffert voor verzending, wat betekent dat het de latency verhoogt.
Om meer te leren over de verschillen tussen UDP en TCP in de context van multiplayer-games, kan je het artikel van Glenn Fiedler lezen .
Eigen Protocol
Dus, je wilt een eigen transportprotocol creƫren, maar weet niet waar te beginnen? Je hebt geluk, want Glenn Fiedler heeft daarover twee geweldige artikelen geschreven. Daarin vind je veel slimme ideeƫn.
Het eerste artikel, uit 2008, is eenvoudiger dan de tweede, uit 2016. Ik raad je aan om met de oudere te beginnen.
Houd er rekening mee dat Glenn Fiedler een grote voorstander is van het gebruik van een eigen protocol op basis van UDP. En na het lezen van zijn artikelen zul je zeker zijn mening over de serieuze tekortkomingen van TCP in videogames overnemen en je eigen protocol willen implementeren.
Maar als je nieuw bent in netwerken, doe jezelf dan een gunst en gebruik TCP of een bibliotheek. Voor een succesvolle implementatie van een eigen transportprotocol moet je vooraf veel leren.
Netwerkbibliotheken
Als je iets efficiƫnters nodig hebt dan TCP, maar je wilt niet de moeite nemen om je eigen protocol te implementeren en in veel details te duiken, dan kun je gebruik maken van een netwerkbibliotheek. Er zijn er heel wat:
- van Glenn Fiedler
- , dat niet meer wordt ondersteund, maar de fork lijkt nog steeds actief te zijn.
- is een bibliotheek die is gemaakt voor multiplayer FPS
- van Valve
Ik heb ze niet allemaal geprobeerd, maar ik geef de voorkeur aan ENet omdat het eenvoudig te gebruiken en betrouwbaar is. Bovendien heeft het duidelijke documentatie en een tutorial voor beginners.
Transportprotocol: conclusie
Samenvattend: er zijn twee basistransportprotocollen: TCP en UDP. TCP heeft veel nuttige functies: betrouwbaarheid, het bewaren van de volgorde van pakketten, foutdetectie. UDP heeft dit allemaal niet, maar TCP heeft van nature hogere latencies, wat onaanvaardbaar kan zijn voor sommige games. Om lage latencies te garanderen, kunt u een eigen protocol op basis van UDP maken of een bibliotheek gebruiken die een transportprotocol op UDP implementeert en aangepast is voor multiplayer-videogames.
De keuze tussen TCP, UDP en een bibliotheek hangt af van verschillende factoren. Ten eerste, van de behoeften van de game: heeft deze lage latencies nodig? Ten tweede, van de vereisten van het applicatieprotocol: heeft het een betrouwbaar protocol nodig? Zoals we in het volgende deel zullen zien, kunt u een applicatieprotocol maken dat goed werkt met een onbetrouwbaar protocol. Tot slot moet ook rekening worden gehouden met de ervaring van de ontwikkelaar van de netwerkengine.
Ik heb twee tips:
- Abstracteer het transportprotocol maximaal van de rest van de applicatie, zodat het eenvoudig kan worden vervangen zonder de hele code opnieuw te schrijven.
- Voorkom premature optimalisatie. Als u geen netwerkdeskundige bent en niet zeker weet of u een eigen transportprotocol op basis van UDP nodig heeft, kunt u beginnen met TCP of een bibliotheek die betrouwbaarheid biedt, en vervolgens de prestaties testen en meten. Als er problemen optreden en u zeker weet dat de oorzaak in het transportprotocol ligt, is het misschien tijd om een eigen transportprotocol te creƫren.
Ter afsluiting van dit deel raad ik u aan te lezen van Brian Hook, waarin veel van de onderwerpen die hier worden besproken aan bod komen.
Applicatieprotocol
Nu we gegevens tussen de clients en de server kunnen uitwisselen, moeten we beslissen welke gegevens precies moeten worden verzonden en in welk formaat.
De klassieke opzet is dat clients invoer of acties naar de server sturen, terwijl de server de huidige status van de game naar de clients stuurt.
De server verzendt geen volledige, maar gefilterde toestand met de entiteiten die zich in de buurt van de speler bevinden. Dit doet hij om drie redenen. Ten eerste kan de volledige toestand te groot zijn om met hoge frequentie te verzenden. Ten tweede zijn klanten voornamelijk geĆÆnteresseerd in visuele en audio-informatie, omdat het grootste deel van de game-logica op de gameserver wordt gesimuleerd. Ten derde mag de speler in sommige games bepaalde gegevens niet weten, zoals de positie van de tegenstander aan de andere kant van de kaart; anders kan hij pakketten afluisteren en precies weten waar hij naartoe moet bewegen om te winnen.
Serialisatie
De eerste stap is het omzetten van de gegevens die we willen verzenden (de invoer of de game-toestand) naar een geschikt formaat voor verzending. Dit proces wordt .
direct in je opkomt: gebruik een menselijk leesbaar formaat, zoals JSON of XML. Maar dit zal totaal inefficiƫnt zijn en veel van de bandbreedte verspillen.
Daarom wordt aangeraden om een binaire indeling te gebruiken die veel compacter is. Dat betekent dat de pakketten slechts enkele bytes zullen bevatten. Hier moet men rekening houden met het probleem van , die op verschillende computers kan verschillen.
Voor serialisatie van gegevens kan men een bibliotheek gebruiken, bijvoorbeeld:
- van Google
- van Sandstorm
- van Shane Grant en Randolph B. Hurst
Zorg ervoor dat de bibliotheek draagbare archieven maakt en zich bezighoudt met de byte-volgorde.
Een alternatieve oplossing kan een zelfgemaakte implementatie zijn, die niet al te moeilijk is, vooral als je in de code een datagerichte aanpak gebruikt. Bovendien stelt het je in staat om optimalisaties uit te voeren die niet altijd mogelijk zijn bij het gebruik van een bibliotheek.
Glenn Fiddler heeft twee artikelen over serialisatie geschreven: en .
Compressie
De hoeveelheid gegevens die tussen clients en de server wordt verzonden, is beperkt door de bandbreedte van de verbinding. Gegevenscompressie zal het mogelijk maken meer gegevens in elke snapshot te verzenden, de updatefrequentie te verhogen of gewoon de eisen aan de bandbreedte te verlagen.
Bitpacking
De eerste techniek is bitpacking. Dit houdt in dat precies het aantal bits dat nodig is om de gewenste hoeveelheid te beschrijven wordt gebruikt. Bijvoorbeeld, als je een enumeratie hebt die 16 verschillende waarden kan hebben, dan kun je in plaats van een volledige byte (8 bits) slechts 4 bits gebruiken.
Glenn Fiddler legt in het tweede deel van het artikel uit hoe je dit kunt implementeren. .
Bitpacking werkt bijzonder goed met discretisatie, dat het onderwerp van de volgende sectie zal zijn.
Discretisatie
is een techniek voor compressie met verlies, die inhoudt dat bij het coderen van een hoeveelheid slechts een subset van mogelijke waarden wordt gebruikt. Discretisatie kan het beste worden geĆÆmplementeerd door getallen met een zwevende komma af te ronden.
Glenn Fiddler (weer!) laat zien hoe je discretisatie in de praktijk kunt toepassen in zijn artikel. .
Compressie-algoritmen
De volgende techniek zijn verliesloze compressie-algoritmen.
Hier zijn volgens mij drie van de meest interessante algoritmen om te weten:
- met vooraf berekende codes, die extreem snel zijn en goede resultaten kunnen opleveren. Het werd gebruikt voor het comprimeren van pakketten in de netwerkmotor van Quake3.
- is een algemeen compressie-algoritme dat nooit de dataomvang vergroot. Zoals je kunt zien, werd het in veel toepassingsgebieden gebruikt. Voor statusupdates kan het overbodig blijken, maar het kan nuttig zijn als je assets, lange teksten of terreinen van de server naar klanten moet verzenden.
- is misschien het eenvoudigste compressie-algoritme, maar het is zeer effectief voor bepaalde soorten gegevens en kan worden gebruikt als een voorbereidende stap vóór zlib. Het is bijzonder geschikt voor de compressie van terreinen bestaande uit tegels of voxels, waarin veel aangrenzende elementen zich herhalen.
Delta-compressie
De laatste compressietechniek is delta-compressie. Hierbij worden alleen de verschillen tussen de huidige spelstatus en de laatste status die de client heeft ontvangen, verzonden.
Het werd voor het eerst gebruikt in de netwerkmotor van Quake3. Hier zijn twee artikelen die uitleggen hoe het te gebruiken:
- van Bryan Hook
- van Fabien Sanglard [ artikelen op HabrƩ, zie sectie 'Netwerkmodel']
Glenn Fiddler heeft het ook gebruikt in het tweede deel van zijn artikel. .
Versleuteling
Daarnaast kan het nodig zijn om de overdracht van informatie tussen cliƫnten en de server te versleutelen. Hier zijn verschillende redenen voor:
- privacy/confidentialiteit: berichten kunnen alleen door de ontvanger worden gelezen, en niemand anders die netwerkverkeer sniffen, kan ze lezen.
- authenticatie: iemand die de rol van speler wil vervullen, moet zijn sleutel kennen.
- fraude voorkomen: het wordt veel moeilijker voor kwaadwillende spelers om hun eigen pakketten voor bedrog te creƫren; ze moeten het versleutelingsschema reproduceren en de sleutel vinden (die bij elke verbinding wisselt).
Ik raad ten zeerste aan om hiervoor een bibliotheek te gebruiken. Ik stel voor om gebruik te maken van , omdat deze bijzonder eenvoudig is en geweldige tutorials heeft. Vooral de tutorial over , die het mogelijk maakt om nieuwe sleutels bij elke nieuwe verbinding te genereren.
Applicatieprotocol: conclusie
Hiermee sluiten we het applicatieprotocol af. Ik denk dat compressie helemaal niet noodzakelijk is en dat de beslissing om dit te gebruiken alleen afhangt van het spel en de vereiste bandbreedte. Versleuteling vind ik echter verplicht, maar in de eerste prototype kan dit worden weggelaten.
Logica van de applicatie
Nu zijn we in staat om de status in de cliƫnt bij te werken, maar we kunnen problemen met vertragingen tegenkomen. Een speler moet wachten op een statusupdate van de server nadat hij invoer heeft gedaan, om te zien welke impact hij op de wereld heeft gehad.
Bovendien is de wereld tussen twee statusupdates volledig statisch. Als de frequentie van statusupdates laag is, zullen bewegingen erg schokkerig zijn.
Er zijn verschillende technieken om de impact van dit probleem te verminderen, en in het volgende gedeelte zal ik deze bespreken.
Technieken voor het gladstrijken van vertragingen
Alle technieken die in dit gedeelte worden beschreven, worden uitvoerig behandeld in de serie van Gabriel Gambetta. Ik raad ten zeerste aan om deze geweldige serie artikelen te lezen. Het bevat ook een interactieve demo die laat zien hoe deze technieken in de praktijk werken.
De eerste techniek is om het resultaat van de invoer direct toe te passen, zonder op een antwoord van de server te wachten. Dit wordt client-side voorspelling genoemd.. Maar wanneer de cliƫnt een update van de server ontvangt, moet hij ervoor zorgen dat zijn voorspelling juist was. Als dit niet het geval is, moet hij gewoon zijn toestand aanpassen volgens wat hij van de server heeft ontvangen, omdat de server autoritair is. Deze techniek werd voor het eerst gebruikt in Quake. Meer hierover is te lezen in een artikel. van Fabien Sanglard [ op Habr].
De tweede set technieken wordt gebruikt om de beweging van andere entiteiten tussen twee statusupdates te verwassen. 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 getoond. Het nadeel is dat dit een kleine mate van vertraging met zich meebrengt, omdat de cliƫnt altijd ziet wat er in het verleden is gebeurd. Extrapolatie houdt in dat er wordt voorspeld waar entiteiten nu zouden moeten zijn op basis van de laatste toestand die de cliƫnt heeft ontvangen. Het nadeel hiervan is dat als een entiteit volledig van richting verandert, er een grote fout kan optreden tussen de voorspelling en de werkelijke positie.
De laatste, meest geavanceerde techniek, die alleen nuttig is in FPS-games, is vertragingcompensatie. Bij het gebruik van vertragingcompensatie houdt de server rekening met de vertragingen van de cliƫnt wanneer hij op een doel schiet. Bijvoorbeeld, als een speler een headshot uitvoert op zijn scherm, maar in werkelijkheid, vanwege de vertraging, was zijn doel zich op een andere plaats bevond, zou het onsportief zijn om de speler het recht op de kill te ontzeggen 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 de botsing tussen zijn schot en het doel te controleren.
Glenn Fiddler (zoals altijd!) schreef in 2004 een artikel , waarin hij de basis legde voor de synchronisatie van de fysicasimulatie tussen de server en de cliƫnt. In 2014 schreef hij een nieuwe serie artikelen , waarin hij andere technieken voor synchrone fysicasimulatie beschrijft.
Ook op de wiki van Valve zijn er twee artikelen, en , waarin vertragingcompensatie wordt besproken.
Voorkoming van cheaten
Er zijn twee hoofdtechnieken voor het voorkomen van cheaten.
Eerste: de complexiteit van de verzending van kwaadaardige pakketten door cheaters. Zoals eerder vermeld, is een goede manier om dit te implementeren encryptie.
Tweede: de autoritaire server moet alleen commando's/invoer/acties ontvangen. De cliƫnt mag de staat op de server niet veranderen, behalve door invoer te verzenden. De server moet elke keer na ontvangst van invoer deze op geldigheid controleren voordat deze wordt toegepast.
Applicatielogica: conclusie
Ik raad je aan een manier te implementeren om grote vertragingen en lage vernieuwingsfrequenties te simuleren, zodat je het gedrag van je spel onder slechte omstandigheden kunt testen, zelfs wanneer de cliƫnt en server op dezelfde computer worden uitgevoerd. Dit zal de implementatie van technieken voor het gladstrijken van vertragingen aanzienlijk vereenvoudigen.
Andere nuttige bronnen
Als je andere bronnen wilt verkennen over netmodellen, dan zijn die hier te vinden:
- ā het is de moeite waard om zijn hele blog te lezen, er staan veel geweldige artikelen in. alle artikelen over netwerktechnologieĆ«n zijn verzameld.
- van M. Fatih MAR ā dit is een gedetailleerde lijst van artikelen en video's over netwerken in videogames.
- In heeft ook veel nuttige links.
Bron: habr.com
