Å spille et flerspillerspill er alltid morsomt. I stedet for å slå AI-kontrollerte motstandere, må spilleren møte strategier opprettet av et annet menneske. Denne opplæringen presenterer implementeringen av et flerspillerspill som spilles over nettverket ved hjelp av en ikke-autoritativ peer-to-peer (P2P) tilnærming.
Merk: Selv om denne opplæringen er skrevet med AS3 og Flash, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø. Du må ha en grunnleggende forståelse av nettverkskommunikasjon.
Du kan laste ned eller gaffel den endelige koden fra GitHub repo eller zip-kildefilene. Hvis du vil finne unike ressurser for ditt eget spill, sjekk utvalget av spillfordeler over på Envato Market.
Kunst fra Remastered Tyrian Graphics, Iron Plague og Hard Vacuum av Daniel Cook (Lost Garden).
Et flerspiller spill spilt over nettverket kan implementeres ved hjelp av flere forskjellige tilnærminger, som kan kategoriseres i to grupper: autoritær og ikke-autoritativ.
I den autoritative gruppen er den vanligste tilnærmingen den klient-server arkitektur, hvor en sentral enhet (den autoritative serveren) kontrollerer hele spillet. Hver klient som er koblet til serveren mottar kontinuerlig data, som lokalt lager en representasjon av spilltilstanden. Det er litt som å se på TV.
Autentisk implementering ved hjelp av klient-server arkitektur.Hvis en klient utfører en handling, for eksempel å flytte fra ett punkt til et annet, blir denne informasjonen sendt til serveren. Serveren sjekker om informasjonen er riktig, og oppdaterer deretter spilltilstanden. Etter det forplanter den informasjonen til alle klienter, slik at de kan oppdatere spillstatusen tilsvarende.
I den ikke-autoritative gruppen er det ingen sentral enhet, og hver peer (game) styrer spillstatusen sin. I en peer-to-peer-tilnærming (P2P) tilnærming, sender en peer data til alle andre jevnaldrende og mottar data fra dem, forutsatt at informasjonen er pålitelig og korrekt (juksfri):
Ikke-autoritativ implementering ved hjelp av P2P-arkitektur.I denne opplæringen presenterer jeg implementeringen av et flerspillerspill som spilles over nettverket ved hjelp av en ikke-autoritativ P2P-tilnærming. Spillet er en deathmatch arena hvor hver spiller styrer et skip som kan skyte og slippe bomber.
Jeg skal fokusere på kommunikasjon og synkronisering av peer-stater. Spillet og nettverkskoden abstraheres så mye som mulig for enkelhets skyld.
Tips: Den autoritative tilnærmingen er sikrere mot utroskap, fordi serveren kontrollerer spillstatusen helt og kan ignorere enhver mistenksom melding, som en enhet som sier at den flyttet 200 piksler når den bare kunne ha flyttet 10.Et ikke-autoritativt multiplayer-spill har ingen sentral enhet for å kontrollere spilltilstanden, slik at hver peer må kontrollere sin egen spilltilstand, kommunisere eventuelle endringer og viktige handlinger til de andre. Som en konsekvens ser spilleren to scenarier samtidig: hans skip beveger seg i henhold til hans inngang og a simulering av alle andre skip kontrollert av motstanderne:
Spillerens skip styres lokalt. Motstanderskipene er simulert basert på nettverkskommunikasjon.Spillerens skipbevegelse og handlinger styres av lokal inngang, slik at spillerens spilltilstand oppdateres nesten umiddelbart. For bevegelsen av alle de andre skipene må spilleren motta en nettverksmelding fra hver motstander som informerer hvor deres skip er.
Disse meldingene tar tid å reise over nettverket fra en datamaskin til en annen, så når spilleren mottar en informasjon som sier at en motstander er skipet er (x, y)
, det er sannsynligvis ikke der lenger - det er derfor det er en simulering:
For å holde simuleringen nøyaktig, er hver peer ansvarlig for forplantning bare informasjonen om skipet, ikke de andre. Dette betyr at hvis spillet har fire spillere - si EN
, B
, C
og D
- spiller EN
er den eneste som er i stand til å informere hvor skipet er EN
er, hvis det ble rammet, hvis det sparket en kule eller droppet en bombe, og så videre. Alle andre spillere vil motta meldinger fra EN
informere om hans handlinger og de vil reagere tilsvarende, så hvis Som
kulen fikk C-ene
skip, da C
vil kringkaste en melding som informerer om at den ble ødelagt.
Som en konsekvens vil hver spiller se alle andre skip (og deres handlinger) i henhold til mottatte meldinger. I en perfekt verden ville det ikke være noen nettverkstid, så meldinger ville komme og gå umiddelbart, og simuleringen ville være ekstremt nøyaktig.
Når latensen øker, blir simuleringen imidlertid unøyaktig. For eksempel spiller EN
skyter og lokalt ser kulen treffer B
skipet, men ingenting skjer; det er fordi EN
s syn på B
er forsinket på grunn av nettverksforsinkelse. Når B
faktisk mottatt EN
bullet melding, B
var i en annen posisjon, så ingen treff ble forplantet.
Et viktig skritt i å implementere spillet og sikre at hver spiller vil kunne se samme simulering nøyaktig, er identifikasjonen av relevante handlinger. Disse handlingene endrer gjeldende spilltilstand, for eksempel flytte fra ett punkt til et annet, slippe en bombe, osv.
I vårt spill er de viktige handlingene:
skyte
(spillerens skip sparket en kule eller en bombe)bevege seg
(spillers skip flyttet)dø
(spillers skip ble ødelagt)Hver handling må sendes over nettverket, så det er viktig å finne en balanse mellom antall handlinger og størrelsen på nettverksmeldingene de vil generere. Jo større meldingen er (det vil si jo flere data den inneholder), jo lengre vil det ta for å bli transportert, fordi det kan trenge mer enn en nettverkspakke.
Korte meldinger krever færre CPU-tid for å pakke, sende og pakke ut. Små nettverksmeldinger fører også til at flere meldinger sendes samtidig, noe som øker gjennomgangen.
Etter at de aktuelle handlingene er kartlagt, er det på tide å gjøre dem reproduserbare uten brukerinngang. Selv om det er et prinsipp for god programvareutvikling, er det kanskje ikke klart fra et flerspiller-spillsynspunkt.
Ved å bruke skytespillet til spillet vårt som et eksempel, hvis det er dypt sammenkoblet med inngangslogikken, er det ikke mulig å gjenbruke samme skytekode i forskjellige situasjoner:
Utføre handlinger uavhengig.Når skytingskoden er koblet fra inngangslogikken, er det for eksempel mulig å bruke samme kode for å skyte spillernes kuler og Motstandernes kuler (når en slik nettverksmelding kommer). Det unngår kodereplikasjon og forhindrer mye hodepine.
De Skip
klassen i spillet vårt har for eksempel ingen multiplayer-kode; det er helt avkoblet. Det beskriver et skip, enten det er lokalt eller ikke. Klassen har imidlertid flere metoder for å manipulere skipet, for eksempel rotere()
og en setter for å endre sin posisjon. Som en konsekvens kan multiplayer-koden rotere et skip på samme måte som brukerinngangskoden gjør - forskjellen er at den ene er basert på lokalinngang, mens den andre er basert på nettverksmeldinger.
Nå som alle relevante handlinger er kartlagt, er det på tide å utveksle meldinger blant kolleger for å lage simuleringen. Før du utveksler data, må en kommunikasjonsprotokoll bli formulert. Når det gjelder en multiplayer-spillkommunikasjon, kan en protokoll defineres som et sett med regler som beskriver hvordan en melding er strukturert, slik at alle kan sende, lese og forstå disse meldingene.
Meldingene som utveksles i spillet vil bli beskrevet som objekter, alle med en obligatorisk eiendom som kalles op
(operasjonskode). De op
brukes til å identifisere meldingstypen og angi egenskapene meldingsobjektet har. Dette er strukturen av alle meldinger:
OP_DIE
meldingen sier at et skip ble ødelagt. Det er x
og y
egenskaper inneholder skipets plassering når det ble ødelagt. MOTSTAND
meldingen inneholder den nåværende plasseringen av et peer-skip. Det er x
og y
egenskaper inneholder skipets koordinater på skjermen, mens vinkel
er skipets nåværende rotasjonsvinkel.OP_SHOT
meldingen sier at et skip sparket noe (en kule eller en bombe). De x
og y
egenskaper inneholder skipets plassering når den avfyres; de dx
og dy
Egenskaper indikerer skipretningen, som sikrer at kulen vil bli replikert i alle jevnaldrende med samme vinkel som fyringsskipet som ble brukt når det sikte på og b
eiendom definerer prosjektilens type (kule
eller bombe
).multiplayer
KlasseFor å organisere multiplayer-koden, oppretter vi en multiplayer
klasse. Det er ansvarlig for å sende og motta meldinger, samt å oppdatere de lokale skipene i henhold til mottatte meldinger for å reflektere dagens tilstand av spillsimuleringen.
Den opprinnelige strukturen, som bare inneholder meldingskoden, er:
offentlig klasse multiplayer offentlig const OP_SHOT: String = "S"; offentlig const OP_DIE: String = "D"; offentlig const OP_POSITION: String = "P"; offentlig funksjon Multiplayer () // Tilkoblingskode ble utelatt. offentlig funksjon sendObject (obj: Object): void // Nettverkskode som ble brukt til å sende objektet ble utelatt.
For alle relevante handlinger som er kartlagt tidligere, må en nettverksmelding sendes, så alle jevnaldrende blir informert om den handlingen.
De OP_DIE
Handlingen skal sendes når spilleren rammes av en kule eller en bombeeksplosjon. Det er allerede en metode i spillkoden som ødelegger spilleren fra skipet når den er rammet, så den oppdateres for å formidle den informasjonen:
offentlig funksjon onPlayerHitByBullet (): void // Destoy player's ship playerShip.kill (); // MULTIPLAYER: // Send en melding til alle andre spillere som informerer // skipet ble ødelagt. multiplayer.sendObject (op: Multiplayer.OP_DIE, x: platerShip.x, y: playerShip.y);
De MOTSTAND
Handlingen skal sendes hver gang spilleren endrer sin nåværende posisjon. Multiplayer-koden er injisert i spillkoden for å formidle den informasjonen også:
offentlig funksjon updatePlayerInput (): void var flyttet: Boolean = false; hvis (wasMoveKeysPressed ()) playerShip.x + = playerShip.direction.x; playerShip.y + = playerShip.direction.y; flyttet = sant; hvis (wasRotateKeysPressed ()) playerShip.rotate (10); flyttet = sant; // MULTIPLAYER: // Hvis spilleren flyttes (eller roteres), formidle informasjonen. hvis (flyttet) multiplayer.sendObject (op: Multiplayer.OP_POSITION, x: playerShip.x, y: playerShip.y, vinkel: playerShip.angle);
Endelig, den OP_SHOT
Handlingen må sendes hver gang spilleren brenner noe. Den sendte meldingen inneholder bulletypen som ble avfyrt, slik at alle fagfolk ser riktig prosjektil:
hvis (varShootingKeysPressed ()) var bulletType: Class = getBulletType (); game.shoot (playerShip, bulletType); // MULTIPLAYER: // Informer alle andre spillere om at vi fyrte et prosjektil. multiplayer.sendObject (op: Multiplayer.OP_SHOT, x: playerShip.x, y: playerShip.y, dx: playerShip.direction.x, dy: playerShip.direction.y, b: bBulletType));
På dette punktet kan hver spiller kontrollere og se sitt skip. Under hetten sendes nettverksmeldingene basert på relevante handlinger. Det eneste manglende stykket er tillegg av motstanderne, slik at hver spiller kan se de andre skipene og samhandle med dem.
I spillet er skipene organisert som en matrise. Denne gruppen hadde bare et enkelt skip (spilleren) til nå. For å skape simuleringen for alle andre spillere, er multiplayer
klassen vil bli endret for å legge til et nytt skip til den gruppen når en ny spiller blir med i arenaen:
offentlig klasse multiplayer offentlig const OP_SHOT: String = "S"; offentlig const OP_DIE: String = "D"; offentlig const OP_POSITION: String = "P"; (...) // Denne metoden er påkalt hver gang en ny bruker slutter seg til arenaen. beskyttet funksjon handleUserAdded (bruker: UserObject): void // Opprett en ny skipsbase på den nye brukerens ID. var skip: Ship = nytt skip (user.id); // Legg til skipet i rekkefølge av allerede eksisterende skip. game.ships.add (skip);
Meldingsutvekslingskoden gir automatisk en unik identifikator for hver spiller (den bruker-ID
i koden ovenfor). Denne identifikasjonen brukes av multiplayer-koden til å skape et nytt skip når en spiller slutter seg til arenaen; På denne måten har hvert skip en unik identifikator. Ved å bruke forfatteridentifikatoren for hver mottatt melding, er det mulig å slå opp skipet i en rekke skip.
Endelig er det på tide å legge til handleGetObject ()
til multiplayer
klasse. Denne metoden er påkalt hver gang en ny melding kommer:
offentlig klasse multiplayer offentlig const OP_SHOT: String = "S"; offentlig const OP_DIE: String = "D"; offentlig const OP_POSITION: String = "P"; (...) // Denne metoden er påkalt hver gang en ny bruker slutter seg til arenaen. beskyttet funksjon handleUserAdded (bruker: UserObject): void // Opprett en ny skipsbase på den nye brukerens ID. var skip: Ship = nytt skip (user.id); // Legg til skipet i rekkefølge av allerede eksisterende skip. game.ships.add (skip); beskyttet funksjon handleGetObject (userId: String, data: Object): void var opCode: String = data.op; // Finn skipet til spilleren som sendte meldingen var sendt: Ship = getShipById (userId); bytte (opCode) tilfelle OP_POSITION: // Melding for å oppdatere forfatterens skipposisjon. ship.x = data.x; ship.y = data.y; ship.angle = data.angle; gå i stykker; saken OP_SHOT: // Melding som informerer forfatterens skip slått av et prosjekt. // Først og fremst oppdaterer du skipets posisjon og retning. ship.x = data.x; ship.y = data.y; ship.direction.x = data.dx; ship.direction.y = data.dy; // Brann prosjektilet fra forfatterens skipsted. game.shoot (skip, data.b); gå i stykker; saken OP_DIE: // Melding om forfatterens skip ble ødelagt. ship.kill (); gå i stykker;
Når en ny melding kommer, handleGetObject ()
Metoden er påkalt med to parametere: Forfatter-ID (unikt identifikator) og meldingsdata. Analyserer meldingsdataene, blir operasjonskoden ekstrahert, og ut fra det blir alle andre egenskaper også hentet ut.
Ved hjelp av utvunnet data gjengis multiplayer-koden alle handlinger som ble mottatt over nettverket. Tar OP_SHOT
melding som et eksempel, disse er trinnene som ble utført for å oppdatere gjeldende spilltilstand:
bruker-ID
.Skip
posisjon og vinkel i henhold til mottatte data.Skip
s retning i henhold til mottatte data.Som tidligere beskrevet, kobles skjermkoden fra spilleren og inngangslogikken, slik at prosjektilene slås opp akkurat som en fyret av spilleren lokalt.
Hvis spillet bare flytter enheter basert på nettverksoppdateringer, vil enhver tapt eller forsinket melding føre til at enheten "teleporterer" fra ett punkt til et annet. Det kan mildnes med lokale spådommer.
Ved hjelp av interpolering, for eksempel, blir entitetsbevegelsen interpolert lokalt fra ett punkt til et annet (begge mottas av nettverksoppdateringer). Som et resultat vil entiteten bevege seg jevnt mellom disse punktene. Ideelt sett bør latens ikke overstige tiden som et foretak tar for å bli interpolert fra ett punkt til et annet.
Et annet triks er ekstrapolering, som lokalt beveger enheter basert på sin nåværende tilstand. Det antas at enheten ikke vil endre sin nåværende rute, så det er trygt å få det til å bevege seg i henhold til gjeldende retning og hastighet, for eksempel. Hvis latensen ikke er for høy, reproduserer ekstrapoleringen nøyaktig den forventede bevegelsen til en ny nettverksoppdatering, som resulterer i et glatt bevegelsesmønster.
Til tross for disse triksene kan nettverksforsinkelsen være ekstremt høy og uhåndterlig noen ganger. Den enkleste tilnærmingen til å eliminere det er å koble fra problematiske jevnaldrende. En sikker tilnærming til det er å bruke en timeout: Hvis samtalen tar mer enn en bestemt tid for å svare, blir den koblet fra.
Å gjøre et multiplayer spill spilt over nettverket er en utfordrende og spennende oppgave. Det krever en annen måte å se ting på siden alle relevante handlinger må sendes og reproduseres av alle jevnaldrende. Som en følge av dette, ser alle spillere en simulering av hva som skjer, bortsett fra det lokale skipet, som ikke har noen nettverkslatens.
Denne opplæringen beskrev implementeringen av et flerspillerspill med en ikke-autoritativ P2P-tilnærming. Alle konseptene som presenteres kan utvides for å implementere ulike multiplayer-mekanikere. La multiplayer spillet lage begynne!