Giocare a un gioco multiplayer è sempre divertente. Invece di battere gli avversari controllati dall'IA, il giocatore deve affrontare le strategie create da un altro essere umano. Questo tutorial presenta l'implementazione di un gioco multiplayer giocato in rete usando un approccio peer-to-peer (P2P) non autorevole.
Nota: Sebbene questo tutorial sia scritto usando AS3 e Flash, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi. È necessario avere una conoscenza di base della comunicazione di rete.
Puoi scaricare o imporre il codice finale dal repository GitHub o dai file sorgente compressi. Se vuoi trovare risorse uniche per il tuo gioco, controlla la selezione delle risorse di gioco su Envato Market.
Arte da Remastered Tyrian Graphics, Iron Plague e Hard Vacuum di Daniel Cook (Lost Garden).
Un gioco multiplayer giocato sulla rete può essere implementato utilizzando diversi approcci diversi, che possono essere classificati in due gruppi: autorevole e non autorevole.
Nel gruppo autorevole, l'approccio più comune è il architettura client-server, dove un'entità centrale (il server autorevole) controlla l'intero gioco. Ogni client connesso al server riceve costantemente dati, creando localmente una rappresentazione dello stato del gioco. È un po 'come guardare la TV.
Implementazione autorevole mediante l'architettura client-server.Se un client esegue un'azione, come lo spostamento da un punto a un altro, tale informazione viene inviata al server. Il server controlla se le informazioni sono corrette, quindi aggiorna il suo stato di gioco. Dopo di che diffonde le informazioni a tutti i client, in modo che possano aggiornare di conseguenza il loro stato di gioco.
Nel gruppo non autorevole, non esiste un'entità centrale e ogni peer (gioco) controlla il suo stato di gioco. In un approccio peer-to-peer (P2P), un peer invia i dati a tutti gli altri peer e riceve i dati da loro, supponendo che le informazioni siano affidabili e corrette (senza barare):
Implementazione non autorevole mediante l'architettura P2P.In questo tutorial presenterò l'implementazione di un gioco multiplayer giocato sulla rete utilizzando un approccio P2P non autorevole. Il gioco è un'arena deathmatch in cui ogni giocatore controlla una nave in grado di sparare e sganciare bombe.
Mi concentrerò sulla comunicazione e sincronizzazione degli stati pari. Il gioco e il codice di rete sono astratti il più possibile a scopo di semplificazione.
Mancia: l'approccio autoritario è più sicuro contro il cheating, perché il server controlla completamente lo stato del gioco e può ignorare qualsiasi messaggio sospetto, come un'entità che dice che ha spostato 200 pixel quando avrebbe potuto essere spostato solo 10.Un gioco multiplayer non autorevole non ha un'entità centrale per controllare lo stato del gioco, quindi ogni peer deve controllare il proprio stato di gioco, comunicando eventuali cambiamenti e azioni importanti agli altri. Di conseguenza, il giocatore vede due scenari contemporaneamente: la sua nave si muove secondo il suo input e a simulazione di tutte le altre navi controllate dagli avversari:
La nave del giocatore è controllata localmente. Le navi avversarie sono simulate in base alla comunicazione di rete.I movimenti e le azioni della nave del giocatore sono guidati da input locali, quindi lo stato di gioco del giocatore viene aggiornato quasi istantaneamente. Per il movimento di tutte le altre navi, il giocatore deve ricevere un messaggio di rete da ogni avversario che informa dove sono le loro navi.
Quei messaggi richiedono tempo per viaggiare attraverso la rete da un computer a un altro, quindi quando il giocatore riceve un'informazione che dice che la nave di un avversario è a (x, y)
, probabilmente non c'è più - ecco perché è una simulazione:
Per mantenere la simulazione accurata, ogni peer è responsabile della propagazione solo le informazioni sulla sua nave, non sulle altre. Ciò significa che, se il gioco ha quattro giocatori - per esempio UN
, B
, C
e D
- giocatore UN
è l'unico in grado di informare dove nave UN
è, se è stato colpito, se ha sparato un proiettile o lasciato cadere una bomba, e così via. Tutti gli altri giocatori riceveranno messaggi da UN
informando sulle sue azioni e reagiranno di conseguenza, quindi se Come
pallottola C di
nave, quindi C
trasmetterà un messaggio che informa che è stato distrutto.
Di conseguenza, ogni giocatore vedrà tutte le altre navi (e le loro azioni) in base ai messaggi ricevuti. In un mondo perfetto, non ci sarebbe alcuna latenza di rete, quindi i messaggi andrebbero e vengono istantaneamente e la simulazione sarebbe estremamente accurata.
Con l'aumentare della latenza, tuttavia, la simulazione diventa inaccurata. Ad esempio, giocatore UN
spara e localmente vede il proiettile colpire B
la nave, ma non succede nulla; è perché UN
vista di B
è in ritardo a causa del ritardo della rete. quando B
effettivamente ricevuto UN
messaggio di proiettile, B
era in una posizione diversa, quindi nessun colpo è stato propagato.
Un passo importante nell'implementazione del gioco e nell'assicurare che ogni giocatore sia in grado di vedere con precisione la stessa simulazione è l'identificazione delle azioni pertinenti. Quelle azioni cambiano lo stato attuale del gioco, come spostarsi da un punto a un altro, far cadere una bomba, ecc.
Nel nostro gioco, le azioni importanti sono:
sparare
(la nave del giocatore ha sparato un proiettile o una bomba)mossa
(la nave del giocatore si è spostata)morire
(la nave del giocatore è stata distrutta)Ogni azione deve essere inviata attraverso la rete, quindi è importante trovare un equilibrio tra la quantità di azioni e la dimensione dei messaggi di rete che genereranno. Più grande è il messaggio (cioè più dati contiene), più tempo ci vorrà per essere trasportato, perché potrebbe essere necessario più di un pacchetto di rete.
Brevi messaggi richiedono meno CPU per il confezionamento, l'invio e lo spacchettamento. I messaggi di rete di piccole dimensioni comportano inoltre l'invio di più messaggi contemporaneamente, il che aumenta il throughput.
Dopo aver mappato le azioni pertinenti, è il momento di renderle riproducibili senza l'input dell'utente. Anche se questo è un principio di buona ingegneria del software, potrebbe non essere ovvio dal punto di vista del gioco multiplayer.
Usando l'azione di tiro del nostro gioco come esempio, se è profondamente interconnesso con la logica di input, non è possibile riutilizzare lo stesso codice di ripresa in diverse situazioni:
Esecuzione di azioni in modo indipendente.Quando il codice di scatto è disaccoppiato dalla logica di input, ad esempio, è possibile utilizzare lo stesso codice per sparare ai proiettili del giocatore e i proiettili dell'avversario (quando arriva un messaggio di rete). Evita la replica del codice e previene un sacco di mal di testa.
Il Nave
la classe nel nostro gioco, per esempio, non ha codice multiplayer; è completamente disaccoppiato. Descrive una nave, sia essa locale o meno. La classe, tuttavia, ha diversi metodi per manipolare la nave, come ad esempio ruotare()
e un setter per cambiare la sua posizione. Di conseguenza, il codice multiplayer può ruotare una nave nello stesso modo del codice di input dell'utente - la differenza è che uno è basato sull'input locale, mentre l'altro è basato sui messaggi di rete.
Ora che tutte le azioni rilevanti sono mappate, è il momento di scambiare messaggi tra i peer per creare la simulazione. Prima di scambiare dati, è necessario formulare un protocollo di comunicazione. Per quanto riguarda la comunicazione di giochi multiplayer, un protocollo può essere definito come un insieme di regole che descrivono come è strutturato un messaggio, in modo che tutti possano inviare, leggere e comprendere quei messaggi.
I messaggi scambiati nel gioco saranno descritti come oggetti, tutti contenenti una proprietà obbligatoria chiamata operazione
(codice operazione). Il operazione
è usato per identificare il tipo di messaggio e indicare le proprietà dell'oggetto messaggio. Questa è la struttura di tutti i messaggi:
OP_DIE
messaggio afferma che una nave è stata distrutta. Suo X
e y
le proprietà contengono la posizione della nave quando è stata distrutta. OPPOSIZIONE
messaggio contiene la posizione corrente della nave di un pari. Suo X
e y
le proprietà contengono le coordinate della nave sullo schermo, mentre angolo
è l'angolo di rotazione corrente della nave.OP_SHOT
messaggio afferma che una nave ha sparato qualcosa (un proiettile o una bomba). Il X
e y
le proprietà contengono la posizione della nave quando ha sparato; il dx
e dy
le proprietà indicano la direzione della nave, che assicura che il proiettile sia replicato in tutti i peer usando lo stesso angolo della nave usata quando stava mirando; e il B
proprietà definisce il tipo del proiettile (proiettile
o bomba
).Multiplayer
ClassePer organizzare il codice multiplayer, creiamo a Multiplayer
classe. È responsabile dell'invio e della ricezione di messaggi e dell'aggiornamento delle navi locali in base ai messaggi ricevuti per riflettere lo stato corrente della simulazione del gioco.
La sua struttura iniziale, contenente solo il codice del messaggio, è:
multiplayer di classe pubblica public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; funzione pubblica Multiplayer () // Il codice di connessione è stato omesso. public function sendObject (obj: Object): void // Il codice di rete utilizzato per inviare l'oggetto è stato omesso.
Per ogni azione rilevante mappata in precedenza, è necessario inviare un messaggio di rete, in modo che tutti i peer vengano informati di tale azione.
Il OP_DIE
l'azione deve essere inviata quando il giocatore viene colpito da un proiettile o un'esplosione di una bomba. C'è già un metodo nel codice del gioco che distrugge la nave del giocatore quando viene colpito, quindi viene aggiornato per propagare tali informazioni:
public function onPlayerHitByBullet (): void // Destoy player ship playerShip.kill (); // MULTIPLAYER: // Invia un messaggio a tutti gli altri giocatori che informano che // la nave è stata distrutta. multiplayer.sendObject (op: Multiplayer.OP_DIE, x: platerShip.x, y: playerShip.y);
Il OPPOSIZIONE
l'azione dovrebbe essere inviata ogni volta che il giocatore cambia la sua posizione corrente. Il codice multiplayer è iniettato nel codice di gioco per propagare anche queste informazioni:
public function updatePlayerInput (): void var moved: Boolean = false; if (wasMoveKeysPressed ()) playerShip.x + = playerShip.direction.x; playerShip.y + = playerShip.direction.y; spostato = vero; if (wasRotateKeysPressed ()) playerShip.rotate (10); spostato = vero; // MULTIPLAYER: // Se il giocatore si sposta (o ruota), propagare le informazioni. if (spostato) multiplayer.sendObject (op: Multiplayer.OP_POSITION, x: playerShip.x, y: playerShip.y, angle: playerShip.angle);
Finalmente, il OP_SHOT
l'azione deve essere inviata ogni volta che il giocatore spara qualcosa. Il messaggio inviato contiene il tipo di proiettile che è stato sparato, così che ogni peer vedrà il proiettile corretto:
if (wasShootingKeysPressed ()) var bulletType: Class = getBulletType (); game.shoot (playerShip, bulletType); // MULTIPLAYER: // Informa tutti gli altri giocatori che abbiamo sparato un proiettile. multiplayer.sendObject (op: Multiplayer.OP_SHOT, x: playerShip.x, y: playerShip.y, dx: playerShip.direction.x, dy: playerShip.direction.y, b: bBulletType));
A questo punto, ogni giocatore è in grado di controllare e vedere la propria nave. Sotto il cofano, i messaggi di rete vengono inviati in base alle azioni pertinenti. L'unico pezzo mancante è l'aggiunta degli avversari, in modo che ogni giocatore possa vedere le altre navi e interagire con loro.
Nel gioco, le navi sono organizzate come una matrice. Quella schiera aveva solo una singola nave (il giocatore) fino ad ora. Per creare la simulazione per tutti gli altri giocatori, il Multiplayer
la classe verrà modificata per aggiungere una nuova nave a quell'array ogni volta che un nuovo giocatore si unisce all'arena:
multiplayer di classe pubblica public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; (...) // Questo metodo viene invocato ogni volta che un nuovo utente si unisce all'arena. funzione protetta handleUserAdded (user: UserObject): void // Crea una nuova base di spedizione sull'ID del nuovo utente. var ship: Ship = new ship (user.id); // Aggiungi la nave alla matrice di navi già esistenti. game.ships.add (nave);
Il codice di scambio di messaggi fornisce automaticamente un identificativo univoco per ogni giocatore (il ID utente
nel codice sopra). Tale identificazione viene utilizzata dal codice multiplayer per creare una nuova nave quando un giocatore si unisce all'arena; in questo modo, ogni nave ha un identificatore univoco. Utilizzando l'identificatore autore di ogni messaggio ricevuto, è possibile cercare quella nave nella serie di navi.
Infine, è il momento di aggiungere il handleGetObject ()
al Multiplayer
classe. Questo metodo viene invocato ogni volta che arriva un nuovo messaggio:
multiplayer di classe pubblica public const OP_SHOT: String = "S"; public const OP_DIE: String = "D"; public const OP_POSITION: String = "P"; (...) // Questo metodo viene invocato ogni volta che un nuovo utente si unisce all'arena. funzione protetta handleUserAdded (user: UserObject): void // Crea una nuova base di spedizione sull'ID del nuovo utente. var ship: Ship = new ship (user.id); // Aggiungi la nave alla matrice di navi già esistenti. game.ships.add (nave); protected function handleGetObject (userId: String, data: Object): void var opCode: String = data.op; // Trova la nave del giocatore che ha inviato il messaggio var ship: Ship = getShipById (userId); switch (opCode) case OP_POSITION: // Messaggio per aggiornare la posizione della nave dell'autore. ship.x = data.x; ship.y = data.y; ship.angle = data.angle; rompere; caso OP_SHOT: // Il messaggio che informa la nave dell'autore ha sparato un proiettile. // Prima di tutto, aggiorna la posizione e la direzione della nave. ship.x = data.x; ship.y = data.y; ship.direction.x = data.dx; ship.direction.y = data.dy; // Spara il proiettile dalla posizione della nave dell'autore. game.shoot (ship, data.b); rompere; caso OP_DIE: // Il messaggio che informa la nave dell'autore è stato distrutto. ship.kill (); rompere;
Quando arriva un nuovo messaggio, il handleGetObject ()
il metodo viene richiamato con due parametri: l'ID autore (identificatore univoco) e i dati del messaggio. Analizzando i dati del messaggio, il codice operazione viene estratto e, in base a ciò, vengono estratte anche tutte le altre proprietà.
Utilizzando i dati estratti, il codice multiplayer riproduce tutte le azioni ricevute sulla rete. Prendendo il OP_SHOT
messaggio come esempio, questi sono i passaggi eseguiti per aggiornare lo stato attuale del gioco:
ID utente
.Nave
Posizione e angolo secondo i dati ricevuti.Nave
direzione secondo i dati ricevuti.Come precedentemente descritto, il codice di tiro è disaccoppiato dal giocatore e dalla logica di input, quindi il proiettile sparato si comporta esattamente come uno sparato dal giocatore localmente.
Se il gioco sposta esclusivamente le entità in base agli aggiornamenti di rete, qualsiasi messaggio perso o ritardato causerà il "teletrasporto" dell'entità da un punto a un altro. Questo può essere mitigato con le previsioni locali.
Utilizzando l'interpolazione, ad esempio, il movimento dell'entità viene interpolato localmente da un punto all'altro (entrambi ricevuti dagli aggiornamenti di rete). Di conseguenza, l'entità si muoverà senza problemi tra quei punti. Idealmente, la latenza non dovrebbe eccedere il tempo impiegato da un'entità per essere interpolato da un punto a un altro.
Un altro trucco è l'estrapolazione, che sposta localmente le entità in base al suo stato corrente. Suppone che l'entità non cambierà la rotta corrente, quindi è sicuro farlo muovere secondo la sua direzione e velocità attuale, per esempio. Se la latenza non è troppo alta, l'estrapolazione riproduce fedelmente il movimento previsto dell'entità fino all'arrivo di un nuovo aggiornamento di rete, determinando un movimento regolare.
Nonostante questi trucchi, la latenza della rete può essere estremamente elevata e talvolta ingestibile. L'approccio più semplice per eliminare è quello di disconnettere i colleghi problematici. Un approccio sicuro è quello di utilizzare un timeout: se il peer richiede più di un tempo specificato per rispondere, viene disconnesso.
Fare un gioco multiplayer giocato in rete è un compito stimolante ed eccitante. Richiede un modo diverso di vedere le cose poiché tutte le azioni pertinenti devono essere inviate e riprodotte da tutti i colleghi. Di conseguenza, tutti i giocatori vedono una simulazione di ciò che sta accadendo, ad eccezione della nave locale, che non ha latenza di rete.
Questo tutorial descrive l'implementazione di un gioco multiplayer usando un approccio P2P non autorevole. Tutti i concetti presentati possono essere espansi per implementare diverse meccaniche multiplayer. Inizia la partita in multiplayer!