Een peer-to-peer multiplayer-netwerkgame bouwen

Een multiplayer-game spelen is altijd leuk. In plaats van AI-gestuurde tegenstanders te verslaan, moet de speler worden geconfronteerd met strategieën die door een ander mens zijn gecreëerd. Deze tutorial presenteert de implementatie van een multiplayer-game die via het netwerk wordt gespeeld met behulp van een niet-gezaghebbende peer-to-peer (P2P) -aanpak.

Notitie: Hoewel deze tutorial geschreven is met behulp van AS3 en Flash, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken. U moet een basiskennis hebben van netwerkcommunicatie.

Je kunt de definitieve code van de GitHub-repo of de gecomprimeerde bronbestanden downloaden of splitten. Als je unieke bronnen voor je eigen spel wilt vinden, bekijk dan de selectie van game-assets op Envato Market.


Eindresultaat voorbeeld

Netwerk demo. controls: pijlen of WASD bewegen, Ruimte schieten, B een bom inzetten.

Kunst van Remastered Tyrian Graphics, Iron Plague en Hard Vacuum van Daniel Cook (Lost Garden).


Invoering

Een multiplayer-game die via het netwerk wordt gespeeld, kan worden geïmplementeerd met behulp van verschillende benaderingen, die kunnen worden onderverdeeld in twee groepen: gezaghebbend en niet-gezaghebbende.

In de gezaghebbende groep is de meest gebruikelijke aanpak de client-server architectuur, waar een centrale entiteit (de gezaghebbende server) de hele game bestuurt. Elke client die op de server is aangesloten, ontvangt voortdurend gegevens en maakt lokaal een representatie van de spelsituatie. Het lijkt een beetje op tv kijken.

Gezaghebbende implementatie met behulp van client-serverarchitectuur.

Als een client een actie uitvoert, zoals het verplaatsen van het ene punt naar het andere, wordt die informatie naar de server verzonden. De server controleert of de informatie correct is en werkt vervolgens de spelstatus bij. Daarna propageert het de informatie naar alle clients, zodat zij hun spelstatus dienovereenkomstig kunnen bijwerken.

In de niet-gezaghebbende groep is er geen centrale entiteit en controleert elke peer (spel) zijn spelstatus. In een peer-to-peer (P2P) -benadering stuurt een peer gegevens naar alle andere peers en ontvangt ze gegevens van hen, ervan uitgaande dat informatie betrouwbaar en correct is (cheat-free):

Niet-bindende implementatie met P2P-architectuur.

In deze tutorial presenteer ik de implementatie van een multiplayer-game die via het netwerk wordt gespeeld met behulp van een niet-gezaghebbende P2P-benadering. Het spel is een deathmatch-arena waar elke speler een schip beheert dat in staat is om bommen te schieten en neer te zetten.

Ik ga me concentreren op de communicatie en synchronisatie van peer-states. De game en de netwerkcode worden zo veel mogelijk geabstraheerd omwille van vereenvoudiging.

Tip: de gezaghebbende aanpak is veiliger tegen valsspelen, omdat de server de spelstatus volledig bestuurt en elk verdacht bericht kan negeren, zoals een entiteit die beweerde dat het 200 pixels bewoog toen het alleen 10 bewogen kon worden.

Een niet-gezaghebbend spel definiëren

Een niet-gezaghebbend multiplayer-spel heeft geen centrale entiteit om de spelstatus te regelen, dus elke peer moet zijn eigen spelstatus regelen en eventuele wijzigingen en belangrijke acties aan de anderen communiceren. Als gevolg hiervan ziet de speler tegelijkertijd twee scenario's: zijn schip beweegt volgens zijn invoer en een simulatie van alle andere schepen gecontroleerd door de tegenstanders:

Het schip van de speler wordt lokaal bestuurd. Tegenpartij schepen worden gesimuleerd op basis van netwerkcommunicatie.

De bewegingen en acties van het schip van de speler worden gestuurd door lokale input, zodat de spelstatus van de speler bijna onmiddellijk wordt bijgewerkt. Voor de verplaatsing van alle andere schepen moet de speler een netwerkbericht ontvangen van elke tegenstander waarin wordt verteld waar zijn schepen zich bevinden.

Die berichten kost tijd om over het netwerk te reizen van de ene computer naar de andere, dus wanneer de speler een informatie ontvangt waarin staat dat het schip van een tegenstander is (x, y), het is er waarschijnlijk niet meer - daarom is het een simulatie:

Communicatie vertraging veroorzaakt door het netwerk.

Om de simulatie accuraat te houden, is elke peer verantwoordelijk voor de verspreiding enkel en alleen de informatie over zijn schip, niet de anderen. Dit betekent dat, als de game vier spelers heeft, bijvoorbeeld EEN, B, C en D - speler EEN is de enige die in staat is om te informeren waar het schip is EEN is, als het geraakt werd, als het een kogel afvuurde of een bom liet vallen, enzovoort. Alle andere spelers ontvangen berichten van EEN informeren over zijn acties en ze zullen dienovereenkomstig reageren, dus als Zoals kogel kreeg C's schip dan C zal een bericht uitzenden met de mededeling dat het is vernietigd.

Bijgevolg ziet elke speler alle andere schepen (en hun acties) op basis van de ontvangen berichten. In een perfecte wereld zou er geen latentie in het netwerk zijn, dus berichten zouden onmiddellijk komen en gaan en de simulatie zou uiterst nauwkeurig zijn.

Naarmate de latentie echter toeneemt, wordt de simulatie onnauwkeurig. Bijvoorbeeld speler EEN schiet en ziet de kogel lokaal raken Bis schip, maar er gebeurt niets; dat is omdat EEN's weergave van B is vertraagd vanwege netwerkvertraging. Wanneer B daadwerkelijk ontvangen EENopsommingsbericht, B was op een andere positie, dus er werd geen treffer gepropageerd.


Relevante acties in kaart brengen

Een belangrijke stap in het implementeren van het spel en ervoor zorgen dat elke speler dezelfde simulatie nauwkeurig kan zien, is de identificatie van relevante acties. Deze acties veranderen de huidige spelstatus, zoals het verplaatsen van het ene punt naar het andere, het laten vallen van een bom, enz.

In onze game zijn de belangrijke acties:

  • schieten (speler's schip heeft een kogel of een bom afgevuurd)
  • verhuizing (speler's schip verplaatst)
  • dood gaan (speler's schip was vernietigd)
Spelersacties tijdens het spel.

Elke actie moet via het netwerk worden verzonden, dus het is belangrijk om een ​​balans te vinden tussen het aantal acties en de grootte van de netwerkberichten die ze zullen genereren. Hoe groter het bericht is (dat wil zeggen, hoe meer gegevens het bevat), hoe langer het duurt om te worden vervoerd, omdat het mogelijk meer dan één netwerkpakket nodig heeft.

Korte berichten vragen minder CPU-tijd om in te pakken, te verzenden en uit te pakken. Kleine netwerkberichten resulteren er ook in dat er meer berichten tegelijkertijd worden verzonden, waardoor de doorvoer toeneemt.


Acties onafhankelijk uitvoeren

Nadat de relevante acties zijn toegewezen, is het tijd om ze reproduceerbaar te maken zonder gebruikersinvoer. Ook al is dat een principe van goede software-engineering, het is misschien niet vanzelfsprekend vanuit het oogpunt van een multiplayer-game.

Als we bijvoorbeeld de schietactie van onze game gebruiken, als deze diep verbonden is met de invoerlogica, is het niet mogelijk om dezelfde opnamecode opnieuw te gebruiken in verschillende situaties:

Acties onafhankelijk uitvoeren.

Wanneer de opnamecode is losgekoppeld van de inputlogica, is het bijvoorbeeld mogelijk om dezelfde code te gebruiken om de kogels van de speler te schieten en de kogels van de tegenstander (wanneer zo'n netwerkbericht binnenkomt). Het vermijdt code-replicatie en voorkomt veel hoofdpijn.

De Schip klasse in onze game heeft bijvoorbeeld geen multiplayer-code; het is volledig ontkoppeld. Het beschrijft een schip, of het nu lokaal is of niet. De klasse heeft echter verschillende methoden voor het manipuleren van het schip, zoals draaien() en een zetter om zijn positie te veranderen. Dientengevolge kan de multiplayercode een schip op dezelfde manier draaien als de gebruikersinvoercode - het verschil is dat de ene gebaseerd is op de lokale invoer, terwijl de andere gebaseerd is op netwerkberichten.


Gegevens uitwisselen op basis van acties

Nu alle relevante acties in kaart zijn gebracht, is het tijd om berichten uit te wisselen tussen de peers om de simulatie te maken. Voordat gegevens worden uitgewisseld, moet een communicatieprotocol worden opgesteld. Met betrekking tot een multiplayer-spelcommunicatie kan een protocol worden gedefinieerd als een set regels die beschrijven hoe een bericht is gestructureerd, zodat iedereen die berichten kan verzenden, lezen en begrijpen.

De berichten die in de game worden uitgewisseld, worden beschreven als objecten die allemaal een verplichte eigenschap bevatten op (operatiecode). De op wordt gebruikt om het berichttype te identificeren en de eigenschappen aan te geven die het berichtobject heeft. Dit is de structuur van alle berichten:

opbouw van netwerkberichten.
  • De OP_DIE bericht staat dat een schip werd vernietigd. Haar X en Y eigenschappen bevatten de locatie van het schip toen het werd vernietigd.
  • De OPPOSITIE bericht bevat de huidige locatie van het schip van een peer. Haar X en Y eigenschappen bevatten de coördinaten van het schip op het scherm, terwijl hoek is de huidige rotatiehoek van het schip.
  • De OP_SHOT bericht vermeldt dat een schip iets heeft afgevuurd (een kogel of een bom). De X en Y eigenschappen bevatten de locatie van het schip wanneer het vuurde; de dx en dy eigenschappen geven de richting van het schip aan, waardoor wordt verzekerd dat de kogel zal worden gerepliceerd in alle peers met dezelfde hoek als het schietschip dat werd gebruikt toen het richtte; en de b eigenschap definieert het type van het projectiel (kogel of bom).

De multiplayer Klasse

Om de multiplayer-code te organiseren, maken we een multiplayer klasse. Het is verantwoordelijk voor het verzenden en ontvangen van berichten, evenals het bijwerken van de lokale schepen op basis van de ontvangen berichten om de huidige status van de spelsimulatie weer te geven.

De initiële structuur, die alleen de berichtcode bevat, is:

public class Multiplayer public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; openbare functie Multiplayer () // Verbindingscode is weggelaten.  openbare functie sendObject (obj: Object): void // Netwerkcode die is gebruikt om het object te verzenden, is weggelaten. 

Actiemeldingen verzenden

Voor elke relevante actie die eerder is toegewezen, moet een netwerkbericht worden verzonden, zodat alle leeftijdsgenoten over die actie worden geïnformeerd.

De OP_DIE actie moet worden verzonden wanneer de speler wordt geraakt door een kogel of een bomexplosie. Er is al een methode in de spelcode die het spelersschip vernietigt wanneer het wordt geraakt, dus het is bijgewerkt om die informatie te verspreiden:

openbare functie onPlayerHitByBullet (): void // Destoy speler's ship playerShip.kill (); // MULTIPLAYER: // Stuur een bericht naar alle andere spelers die informeren // het schip is vernietigd. multiplayer.sendObject (op: Multiplayer.OP_DIE, x: platerShip.x, y: playerShip.y); 

De OPPOSITIE elke keer dat de speler zijn huidige positie verandert, moet actie worden verzonden. De multiplayercode wordt ook in de spelcode geïnjecteerd om die informatie te verspreiden:

public function updatePlayerInput (): void var moved: Boolean = false; if (wasMoveKeysPressed ()) playerShip.x + = playerShip.direction.x; playerShip.y + = playerShip.direction.y; Verplaatst = waar;  if (wasRotateKeysPressed ()) playerShip.rotate (10); Verplaatst = waar;  // MULTIPLAYER: // Als de speler is verplaatst (of geroteerd), verspreidt u de informatie. if (verplaatst) multiplayer.sendObject (op: Multiplayer.OP_POSITION, x: playerShip.x, y: playerShip.y, angle: playerShip.angle); 

eindelijk, de OP_SHOT actie moet worden verzonden telkens wanneer de speler iets ontsteekt. Het verzonden bericht bevat het opsommingstekentype dat is geactiveerd, zodat elke peer het juiste projectiel ziet:

if (wasShootingKeysPressed ()) var bulletType: Class = getBulletType (); game.shoot (playerShip, bulletType); // MULTIPLAYER: // Informeer alle andere spelers dat we een projectiel hebben afgevuurd. multiplayer.sendObject (op: Multiplayer.OP_SHOT, x: playerShip.x, y: playerShip.y, dx: playerShip.direction.x, dy: playerShip.direction.y, b: bBulletType)); 

Synchroniseren op basis van ontvangen gegevens

Op dit punt kan elke speler zijn schip besturen en zien. Onder de motorkap worden de netwerkberichten verzonden op basis van relevante acties. Het enige ontbrekende stuk is de toevoeging van de tegenstanders, zodat elke speler de andere schepen kan zien en ermee kan communiceren.

In het spel zijn de schepen georganiseerd als een array. Die array had tot nu toe slechts een enkel schip (de speler). Om de simulatie voor alle andere spelers te creëren, is de multiplayer klasse zal worden gewijzigd om een ​​nieuw schip aan die array toe te voegen wanneer een nieuwe speler toetreedt tot de arena:

public class Multiplayer public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; (...) // Deze methode wordt aangeroepen telkens wanneer een nieuwe gebruiker toetreedt tot de arena. beschermde functie handleUserAdded (gebruiker: UserObject): void // Maak een nieuwe basis voor het schip op de id van de nieuwe gebruiker. var ship: Ship = new ship (user.id); // Voeg het schip toe aan de reeks bestaande schepen. game.ships.add (schip); 

De berichtenuitwisselingscode biedt automatisch een unieke identificatie voor elke speler (de gebruikersnaam in de code hierboven). Die identificatie wordt door de multiplayercode gebruikt om een ​​nieuw schip te maken wanneer een speler zich bij de arena aansluit; op deze manier heeft elk schip een unieke identificatie. Met behulp van de auteursnaam van elk ontvangen bericht, is het mogelijk om dat schip op te zoeken in de reeks schepen.

Eindelijk is het tijd om de handleGetObject () naar de multiplayer klasse. Deze methode wordt elke keer dat een nieuw bericht binnenkomt aangeroepen:

public class Multiplayer public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; (...) // Deze methode wordt aangeroepen telkens wanneer een nieuwe gebruiker toetreedt tot de arena. beschermde functie handleUserAdded (gebruiker: UserObject): void // Maak een nieuwe basis voor het schip op de id van de nieuwe gebruiker. var ship: Ship = new ship (user.id); // Voeg het schip toe aan de reeks bestaande schepen. game.ships.add (schip);  protected function handleGetObject (userId: String, data: Object): void var opCode: String = data.op; // Zoek het schip van de speler die het bericht heeft verzonden var ship: Ship = getShipById (userId); switch (opCode) case OP_POSITION: // Bericht om de scheepspositie van de auteur bij te werken. ship.x = data.x; ship.y = data.y; ship.angle = data.angle; breken; case OP_SHOT: // Bericht waarin de auteur op de hoogte werd gesteld 'schip heeft een projecel afgevuurd. // Allereerst de scheepspositie en -richting bijwerken. ship.x = data.x; ship.y = data.y; ship.direction.x = data.dx; ship.direction.y = data.dy; // Vuur het projectiel af vanaf de locatie van het schip. game.shoot (schip, data.b); breken; case OP_DIE: // Bericht met informatie over het schip van de auteur werd vernietigd. ship.kill (); breken; 

Wanneer een nieuw bericht arriveert, de handleGetObject () methode wordt aangeroepen met twee parameters: de auteur-ID (unieke ID) en de berichtgegevens. Analyse van de berichtgegevens, de operatiecode wordt geëxtraheerd en op basis daarvan worden ook alle andere eigenschappen geëxtraheerd.

Met behulp van de uitgepakte gegevens reproduceert de multiplayer-code alle acties die via het netwerk zijn ontvangen. Het nemen van de OP_SHOT bericht als voorbeeld, dit zijn de stappen die worden uitgevoerd om de huidige spelstatus bij te werken:

  1. Zoek het lokale schip op geïdentificeerd met gebruikersnaam.
  2. Bijwerken Schippositie en hoek volgens ontvangen gegevens.
  3. Bijwerken Schiprichting volgens ontvangen gegevens.
  4. Roep de spelmethode aan die verantwoordelijk is voor het afvuren van projectielen, het afvuren van een kogel of een bom.

Zoals eerder beschreven, is de schietcode ontkoppeld van de speler en de ingangslogica, dus het projectiel dat wordt afgevuurd gedraagt ​​zich precies zoals het door de speler lokaal wordt afgevuurd..


Verzachtende problemen voorkomen

Als de game uitsluitend entiteiten verplaatst op basis van netwerkupdates, zal een verloren of vertraagd bericht ervoor zorgen dat de entiteit van het ene naar het andere punt 'teleporteert'. Dat kan worden verzacht met lokale voorspellingen.

Met behulp van interpolatie wordt de entiteitsbeweging bijvoorbeeld lokaal geïnterpoleerd van het ene punt naar het andere (beide ontvangen door netwerkupdates). Dientengevolge, zal de entiteit soepel tussen die punten bewegen. Idealiter zou de latentie niet langer mogen zijn dan de tijd die een entiteit nodig heeft om van het ene punt naar het andere te worden geïnterpoleerd.

Een andere truc is extrapolatie, waarbij entiteiten lokaal worden verplaatst op basis van de huidige status. Het gaat ervan uit dat de entiteit de huidige route niet zal veranderen, dus het is veilig om hem te laten bewegen op basis van zijn huidige richting en snelheid, bijvoorbeeld. Als de latentie niet te hoog is, reproduceert de extrapolatie nauwkeurig de verwachte beweging van de entiteit totdat een nieuwe netwerkupdate arriveert, resulterend in een vloeiend bewegingspatroon.

Ondanks die trucs kan de latentie van het netwerk soms extreem hoog en soms onhandelbaar zijn. De gemakkelijkste manier om dit te elimineren, is door de problematische peers los te koppelen. Een veilige benadering hiervoor is om een ​​time-out te gebruiken: als de peer meer dan een bepaalde tijd nodig heeft om te antwoorden, wordt deze verbroken.


Conclusie

Het maken van een multiplayer-game die via het netwerk wordt gespeeld, is een uitdagende en opwindende taak. Het vereist een andere manier om dingen te zien, omdat alle relevante acties door alle leeftijdsgenoten moeten worden verzonden en gereproduceerd. Als gevolg hiervan zien alle spelers een simulatie van wat er gebeurt, behalve het lokale schip, dat geen netwerklatentie heeft.

Deze tutorial beschreef de implementatie van een multiplayer-game met een niet-gezaghebbende P2P-benadering. Alle gepresenteerde concepten kunnen worden uitgebreid om verschillende multiplayer-mechanismen te implementeren. Laat het spel van meerdere spelers beginnen!