Erstellen Sie ein Multiplayer-Piraten-Shooter-Spiel: In Ihrem Browser

() translation by (you can also view the original English article)
Das Erstellen von Multiplayer-Spielen ist aus mehreren Gründen eine Herausforderung: Das Hosting kann teuer, das Design schwierig und die Implementierung schwierig sein. Mit diesem Tutorial hoffe ich, diese letzte Barriere zu überwinden.
Dies richtet sich an Entwickler, die wissen, wie man Spiele erstellt und mit JavaScript vertraut sind, aber noch nie ein Online-Multiplayer-Spiel erstellt haben. Sobald Sie fertig sind, sollten Sie mit der Implementierung grundlegender Netzwerkkomponenten in jedes Spiel vertraut sein und von dort aus darauf aufbauen können!
Das werden wir bauen:



Sie können hier eine Live-Version des Spiels ausprobieren! W oder Auf, um sich in Richtung Maus zu bewegen, und klicken, um zu schießen. (Wenn niemand anderes online ist, öffnen Sie zwei Browserfenster auf demselben Computer oder eines auf Ihrem Telefon, um zu sehen, wie der Mehrspielermodus funktioniert.) Wenn Sie dies lokal ausführen möchten, ist der vollständige Quellcode auch auf GitHub verfügbar.
Ich habe dieses Spiel mit Kenneys Pirate Pack-Kunstelementen und dem Phaser-Spiel-Framework zusammengestellt. In diesem Tutorial übernehmen Sie die Rolle eines Netzwerkprogrammierers. Ihr Ausgangspunkt wird eine voll funktionsfähige Einzelspieler-Version dieses Spiels sein, und es wird Ihre Aufgabe sein, den Server in Node.js zu schreiben und Socket.io als Netzwerkteil zu verwenden. Um dieses Tutorial überschaubar zu halten, werde ich mich auf die Multiplayer-Teile konzentrieren und die spezifischen Konzepte von Phaser und Node.j überfliegen.
Es ist nicht erforderlich, etwas lokal einzurichten, da wir dieses Spiel vollständig im Browser auf Glitch.com erstellen werden! Glitch ist ein großartiges Tool zum Erstellen von Webanwendungen, einschließlich eines Backends, einer Datenbank und allem. Es eignet sich hervorragend für das Prototyping, das Unterrichten und die Zusammenarbeit. Ich freue mich, Ihnen dieses Tutorial vorstellen zu können.
Lassen Sie uns eintauchen.
1. Setup
Ich habe das Starter-Kit auf Glitch.com veröffentlicht.
Einige schnelle Tipps zur Benutzeroberfläche: Sie können jederzeit eine Live-Vorschau Ihrer App anzeigen, indem Sie auf die Schaltfläche Anzeigen (oben links) klicken.



Die vertikale Seitenleiste links enthält alle Dateien in Ihrer App. Um diese App zu bearbeiten, müssen Sie sie "remixen". Dadurch wird eine Kopie davon in Ihrem Konto erstellt (oder in Git-Jargon gegabelt). Klicken Sie auf die Schaltfläche Diesen Remix.



Zu diesem Zeitpunkt bearbeiten Sie die App unter einem anonymen Konto. Sie können sich (oben rechts) anmelden, um Ihre Arbeit zu speichern.
Bevor wir fortfahren, ist es wichtig, sich mit dem Code für das Spiel vertraut zu machen, in dem Sie Multiplayer hinzufügen möchten. Schauen Sie sich index.html an. Neben dem Spielerobjekt (Zeile 35) sind drei wichtige Funktionen zu beachten: preload
(Zeile 99), create
(Zeile 115) und GameLoop
(Zeile 142).
Wenn Sie lieber auf diese Weise lernen möchten, probieren Sie diese Herausforderungen aus, um sicherzustellen, dass Sie einen Überblick über die Funktionsweise des Spiels erhalten:
- Vergrößern Sie die Welt (Zeile 29) - beachten Sie, dass es für die In-Game-Welt eine separate Weltgröße und für die tatsächliche Leinwand auf der Seite eine Fenstergröße gibt.
- Stellen Sie sicher, dass die LEERTASTE auch nach vorne geschoben wird (Zeile 53).
- Ändern Sie den Schiffstyp Ihres Spielers (Zeile 129).
- Bewegen Sie die Kugeln langsamer (Zeile 155).
Socket.io installieren
Socket.io ist eine Bibliothek zum Verwalten der Echtzeitkommunikation im Browser mithilfe von WebSockets (im Gegensatz zur Verwendung eines Protokolls wie UDP, wenn Sie ein Multiplayer-Desktop-Spiel erstellen). Es gibt auch Fallbacks, um sicherzustellen, dass es auch dann noch funktioniert, wenn WebSockets nicht unterstützt werden. Es kümmert sich also um die Messaging-Protokolle und stellt ein nettes ereignisbasiertes Nachrichtensystem zur Verfügung, das Sie verwenden können.
Als erstes müssen wir das Socket.io-Modul installieren. Unter Glitch können Sie dies tun, indem Sie in die Datei package.json gehen und entweder das gewünschte Modul in die Abhängigkeiten eingeben oder auf Package hinzufügen klicken und "socket.io" eingeben.



Dies wäre ein guter Zeitpunkt, um auf die Serverprotokolle hinzuweisen. Klicken Sie links auf die Schaltfläche Protokolle, um das Serverprotokoll aufzurufen. Sie sollten sehen, dass Socket.io zusammen mit all seinen Abhängigkeiten installiert wird. Hier werden Fehler oder Ausgaben des Servercodes angezeigt.



Nun zu server.js. Hier lebt Ihr Servercode. Im Moment gibt es nur ein grundlegendes Boilerplate zum Bereitstellen unseres HTML. Fügen Sie diese Zeile oben hinzu, um Socket.io einzuschließen:
1 |
var io = require('socket.io')(http); // Make sure to put this after http has been defined |
Jetzt müssen wir auch Socket.io in den Client aufnehmen. Gehen Sie also zurück zu index.html und fügen Sie dies oben in Ihr <head>
-Tag ein:
1 |
<!-- Load the Socket.io networking library -->
|
2 |
<script src="/socket.io/socket.io.js"></script> |
Hinweis: Socket.io übernimmt automatisch das Bereitstellen der Clientbibliothek auf diesem Pfad. Aus diesem Grund funktioniert diese Zeile, auch wenn das Verzeichnis /socket.io/ in Ihren Ordnern nicht angezeigt wird.
Jetzt ist Socket.io enthalten und bereit zu gehen!
2. Spieler erkennen und laichen
Unser erster wirklicher Schritt wird darin bestehen, Verbindungen auf dem Server zu akzeptieren und neue Spieler auf dem Client hervorzubringen.
Akzeptieren von Verbindungen auf dem Server
Fügen Sie unten in server.js den folgenden Code hinzu:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
})
|
Dadurch wird Socket.io angewiesen, auf ein connection
-Ereignis zu warten, das automatisch ausgelöst wird, wenn ein Client eine Verbindung herstellt. Es wird ein neues socket
-Objekt für jeden Client erstellt, wobei socket.id
eine eindeutige Kennung für diesen Client ist.
Um sicherzustellen, dass dies funktioniert, kehren Sie zu Ihrem Client (index.html) zurück und fügen Sie diese Zeile irgendwo in der create-Funktion hinzu:
1 |
var socket = io(); // This triggers the 'connection' event on the server |
Wenn Sie das Spiel starten und dann Ihr Serverprotokoll anzeigen (klicken Sie auf die Schaltfläche Protokolle), sollte dieses Verbindungsereignis protokolliert werden!
Wenn sich ein neuer Spieler verbindet, erwarten wir, dass er uns Informationen über seinen Status sendet. In diesem Fall müssen wir mindestens x, y und den Winkel kennen, um sie an der richtigen Stelle richtig zu erzeugen.
Die Ereignis connection
war ein integriertes Ereignis, das Socket.io für uns auslöst. Wir können auf jedes benutzerdefinierte Ereignis warten, das wir möchten. Ich werde meinen new-player
anrufen und ich erwarte, dass der Client ihn sendet, sobald er sich mit Informationen über seinen Standort verbindet. Das würde so aussehen:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
5 |
console.log("New player has state:",state_data); |
6 |
})
|
7 |
})
|
Sie werden noch nichts im Serverprotokoll sehen, wenn Sie dies ausführen. Dies liegt daran, dass wir dem Kunden noch nicht gesagt haben, dass er dieses Ereignis für new-player
auslösen soll. Aber tun wir so, als wäre das für einen Moment erledigt, und machen wir weiter auf dem Server. Was soll passieren, nachdem wir den Standort des neuen Spielers erhalten haben, der beigetreten ist?
Wir könnten jedem anderen verbundenen Spieler eine Nachricht senden, um ihn darüber zu informieren, dass ein neuer Spieler beigetreten ist. Socket.io bietet dazu eine praktische Funktion:
1 |
socket.broadcast.emit('create-player',state_data); |
Wenn Sie socket.emit
aufrufen, wird die Nachricht nur an diesen einen Client zurückgesendet. Durch Aufrufen von socket.broadcast.emit
wird es an jeden mit dem Server verbundenen Client gesendet, mit Ausnahme des einen Sockets, auf dem es aufgerufen wurde.
Bei Verwendung von io.emit
wird die Nachricht ohne Ausnahmen an jeden mit dem Server verbundenen Client gesendet. Wir möchten dies mit unserem aktuellen Setup nicht tun, da es ein doppeltes Sprite geben würde, wenn Sie eine Nachricht vom Server erhalten, in der Sie aufgefordert werden, Ihr eigenes Schiff zu erstellen, da wir das Schiff des eigenen Spielers bereits zu Beginn des Spiels erstellen. Hier ist ein praktisches Cheatsheet für die verschiedenen Arten von Messaging-Funktionen, die wir in diesem Tutorial verwenden werden.
Der Servercode sollte nun folgendermaßen aussehen:
1 |
// Tell Socket.io to start accepting connections
|
2 |
io.on('connection', function(socket){ |
3 |
console.log("New client has connected with id:",socket.id); |
4 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
5 |
console.log("New player has state:",state_data); |
6 |
socket.broadcast.emit('create-player',state_data); |
7 |
})
|
8 |
})
|
Jedes Mal, wenn sich ein Spieler verbindet, erwarten wir, dass er uns eine Nachricht mit seinen Standortdaten sendet, und wir senden diese Daten direkt an jeden anderen Spieler zurück, damit er dieses Sprite erzeugen kann.
Laichen auf dem Client
Um diesen Zyklus abzuschließen, müssen wir zwei Dinge auf dem Client tun:
- Senden Sie eine Nachricht mit unseren Standortdaten, sobald wir eine Verbindung hergestellt haben.
- Hören Sie auf Ereignisse
create-player
und erzeugen Sie einen Spieler an diesem Ort.
Für die erste Aufgabe können wir, nachdem wir den Player in unserer create-Funktion (um Zeile 135) erstellt haben, eine Nachricht mit den Standortdaten ausgeben, die wir wie folgt senden möchten:
1 |
socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation}) |
Sie müssen sich nicht um die Serialisierung der von Ihnen gesendeten Daten kümmern. Sie können jede Art von Objekt übergeben und Socket.io wird es für Sie erledigen.
Bevor Sie fortfahren, testen Sie, ob dies funktioniert. In den Serverprotokollen sollte eine Meldung angezeigt werden, die Folgendes besagt:
1 |
New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 } |
Wir wissen, dass unser Server die Ankündigung erhält, dass ein neuer Spieler eine Verbindung hergestellt hat, und dass er seine Standortdaten korrekt abruft!
Als nächstes möchten wir auf eine Anfrage warten, um einen neuen Player zu erstellen. Wir können diesen Code direkt nach unserer Ausgabe platzieren und er sollte ungefähr so aussehen:
1 |
socket.on('create-player',function(state){ |
2 |
// CreateShip is a function I've already defined to create and return a sprite
|
3 |
CreateShip(1,state.x,state.y,state.angle) |
4 |
})
|
Testen Sie es jetzt. Öffnen Sie zwei Fenster deines Spiels und schau, ob es funktioniert.
Was Sie sehen sollten, ist, dass nach dem Öffnen von zwei Clients der erste Client zwei gespawnte Schiffe hat, während der zweite nur eines sieht.
Herausforderung: Können Sie herausfinden, warum dies geschieht? Oder wie könnten Sie das beheben? Gehen Sie die von uns geschriebene Client / Server-Logik durch und versuchen Sie, sie zu debuggen.
Ich hoffe, Sie hatten die Gelegenheit, selbst darüber nachzudenken! Wenn der erste Spieler eine Verbindung herstellt, sendet der Server ein Ereignis create-player
an jeden anderen Spieler, aber es war kein anderer Spieler da, der es empfangen konnte. Sobald der zweite Spieler eine Verbindung herstellt, sendet der Server seine Sendung erneut, und Spieler 1 empfängt sie und erzeugt das Sprite korrekt, während Spieler 2 die anfängliche Verbindungssendung von Spieler 1 verpasst hat.
Das Problem tritt also auf, weil Spieler 2 spät im Spiel beitritt und den Status des Spiels kennen muss. Wir müssen jedem neuen Spieler, der verbindet, mitteilen, welche Spieler bereits existieren (oder was bereits auf der Welt passiert ist), damit sie aufholen können. Bevor wir damit beginnen, dies zu beheben, habe ich eine kurze Warnung.
Eine Warnung zum Synchronisieren des Spielstatus
Es gibt zwei Ansätze, um das Spiel jedes Spielers synchron zu halten. Die erste besteht darin, nur die minimale Menge an Informationen darüber zu senden, was im Netzwerk geändert wurde. Jedes Mal, wenn sich ein neuer Spieler verbindet, senden Sie nur die Informationen für diesen neuen Spieler an alle anderen Spieler (und senden diesem neuen Spieler eine Liste aller anderen Spieler auf der Welt). Wenn sie die Verbindung trennen, teilen Sie dies allen anderen Spielern mit Dieser einzelne Client hat die Verbindung getrennt.
Der zweite Ansatz besteht darin, den gesamten Spielstatus zu senden. In diesem Fall senden Sie einfach jedes Mal, wenn eine Verbindung hergestellt oder getrennt wird, eine vollständige Liste aller Spieler an alle.
Das erste ist in dem Sinne besser, dass es die über das Netzwerk gesendeten Informationen minimiert, aber es kann sehr schwierig sein und das Risiko birgt, dass Spieler nicht mehr synchron sind. Die zweite garantiert, dass die Spieler immer synchron sind, beinhaltet jedoch das Senden von mehr Daten mit jeder Nachricht.
In unserem Fall können wir, anstatt zu versuchen, Nachrichten zu senden, wenn ein neuer Spieler sich verbunden hat, um ihn zu erstellen, wenn er sich getrennt hat, um ihn zu löschen, und wenn er sich bewegt hat, um seine Position zu aktualisieren, all das in einem update
-Ereignis konsolidieren. Dieses Update-Ereignis sendet immer die Positionen aller verfügbaren Spieler an alle Clients. Das ist alles, was der Server tun muss. Der Kunde ist dann dafür verantwortlich, seine Welt mit dem Zustand, den er erhält, auf dem neuesten Stand zu halten.
Um dies umzusetzen, werde ich:
- Ein Wörterbuch mit Spielern führen, wobei der Schlüssel ihre ID und der Wert ihre Standortdaten sind.
- Den Player zu diesem Wörterbuch hinzufügen, wenn er eine Verbindung herstellt und ein Aktualisierungsereignis sendet.
- Den Player aus diesem Wörterbuch entfernen, wenn er die Verbindung trennt und ein Aktualisierungsereignis sendet.
Sie können versuchen, dies selbst zu implementieren, da diese Schritte ziemlich einfach sind (der Spickzettel kann nützlich sein). So könnte die vollständige Implementierung aussehen:
1 |
// Tell Socket.io to start accepting connections
|
2 |
// 1 - Keep a dictionary of all the players as key/value
|
3 |
var players = {}; |
4 |
io.on('connection', function(socket){ |
5 |
console.log("New client has connected with id:",socket.id); |
6 |
socket.on('new-player',function(state_data){ // Listen for new-player event on this client |
7 |
console.log("New player has state:",state_data); |
8 |
// 2 - Add the new player to the dict
|
9 |
players[socket.id] = state_data; |
10 |
// Send an update event
|
11 |
io.emit('update-players',players); |
12 |
})
|
13 |
socket.on('disconnect',function(){ |
14 |
// 3- Delete from dict on disconnect
|
15 |
delete players[socket.id]; |
16 |
// Send an update event
|
17 |
})
|
18 |
})
|
Die Kundenseite ist etwas kniffliger. Einerseits müssen wir uns jetzt nur um das update-players
-Ereignis kümmern, andererseits müssen wir berücksichtigen, dass mehr Schiffe erstellt werden, wenn der Server uns mehr Schiffe sendet, als wir wissen, oder dass wir zerstören, wenn wir zu viele haben .
So habe ich dieses Ereignis auf dem Client behandelt:
1 |
// Listen for other players connecting
|
2 |
// NOTE: You must have other_players = {} defined somewhere
|
3 |
socket.on('update-players',function(players_data){ |
4 |
var players_found = {}; |
5 |
// Loop over all the player data received
|
6 |
for(var id in players_data){ |
7 |
// If the player hasn't been created yet
|
8 |
if(other_players[id] == undefined && id != socket.id){ // Make sure you don't create yourself |
9 |
var data = players_data[id]; |
10 |
var p = CreateShip(1,data.x,data.y,data.angle); |
11 |
other_players[id] = p; |
12 |
console.log("Created new player at (" + data.x + ", " + data.y + ")"); |
13 |
}
|
14 |
players_found[id] = true; |
15 |
|
16 |
// Update positions of other players
|
17 |
if(id != socket.id){ |
18 |
other_players[id].x = players_data[id].x; // Update target, not actual position, so we can interpolate |
19 |
other_players[id].y = players_data[id].y; |
20 |
other_players[id].rotation = players_data[id].angle; |
21 |
}
|
22 |
|
23 |
|
24 |
}
|
25 |
// Check if a player is missing and delete them
|
26 |
for(var id in other_players){ |
27 |
if(!players_found[id]){ |
28 |
other_players[id].destroy(); |
29 |
delete other_players[id]; |
30 |
}
|
31 |
}
|
32 |
|
33 |
})
|
Ich verfolge die Schiffe auf dem Client in einem Wörterbuch namens other_players
, das ich einfach oben in meinem Skript definiert habe (hier nicht gezeigt). Da der Server die Spielerdaten an alle Spieler sendet, muss ich einen Scheck hinzufügen, damit ein Client kein zusätzliches Sprite für sich selbst erstellt. (Wenn Sie Probleme bei der Strukturierung haben, finden Sie hier den vollständigen Code, der sich zu diesem Zeitpunkt in index.html befinden sollte).
Testen Sie dies jetzt. Sie sollten in der Lage sein, mehrere Clients zu erstellen und zu schließen und die richtige Anzahl von Schiffen an den richtigen Positionen zu sehen!
3. Schiffspositionen synchronisieren
Hier kommen wir zum wirklich lustigen Teil. Wir möchten jetzt die Positionen der Schiffe für alle Kunden synchronisieren. Hier zeigt sich wirklich die Einfachheit der Struktur, die wir bisher aufgebaut haben. Wir haben bereits ein Update-Ereignis, mit dem alle Standorte synchronisiert werden können. Jetzt müssen wir nur noch:
- Lassen Sie den Client jedes Mal senden, wenn er mit seinem neuen Standort umgezogen ist.
- Lassen Sie den Server auf diese Verschiebungsnachricht warten und aktualisieren Sie den Eintrag dieses Spielers im
players
-Wörterbuch. - Senden Sie ein Update-Ereignis an alle Clients.
Und das sollte es sein! Jetzt sind Sie an der Reihe, dies selbst zu implementieren.
Wenn Sie nicht weiterkommen und einen Hinweis benötigen, können Sie sich das endgültig abgeschlossene Projekt als Referenz ansehen.
Hinweis zum Minimieren von Netzwerkdaten
Der einfachste Weg, dies zu implementieren, besteht darin, alle Spieler jedes Mal mit den neuen Positionen zu aktualisieren, wenn Sie eine Bewegungsnachricht von einem Spieler erhalten. Dies ist insofern großartig, als die Spieler immer die neuesten Informationen erhalten, sobald diese verfügbar sind. Die Anzahl der über das Netzwerk gesendeten Nachrichten kann jedoch leicht auf Hunderte pro Frame ansteigen. Stellen Sie sich vor, Sie hätten 10 Spieler, von denen jeder in jedem Frame eine Bewegungsnachricht sendet, die der Server dann an alle 10 Spieler zurücksenden muss. Das sind schon 100 Nachrichten pro Frame!
Eine bessere Möglichkeit besteht darin, zu warten, bis der Server alle Nachrichten von den Spielern erhalten hat, bevor Sie ein großes Update mit allen Informationen an alle Spieler senden. Auf diese Weise reduzieren Sie die Anzahl der Nachrichten, die Sie senden, auf die Anzahl der Spieler, die Sie im Spiel haben (im Gegensatz zum Quadrat dieser Anzahl). Das Problem dabei ist jedoch, dass jeder so viel Verzögerung hat wie der Spieler mit der langsamsten Verbindung im Spiel.
Eine andere Möglichkeit besteht darin, den Server einfach Updates mit einer konstanten Rate senden zu lassen, unabhängig davon, wie viele Nachrichten er bisher von Spielern erhalten hat. Die Aktualisierung des Servers etwa 30 Mal pro Sekunde scheint ein gängiger Standard zu sein.
Wie auch immer Sie sich entscheiden, Ihren Server zu strukturieren, achten Sie darauf, wie viele Nachrichten Sie in jedem Frame frühzeitig senden, wenn Sie Ihr Spiel entwickeln.
4. Aufzählungszeichen synchronisieren
Wir sind fast da! Das letzte große Stück wird darin bestehen, die Aufzählungszeichen über das Netzwerk zu synchronisieren. Wir könnten es genauso machen, wie wir die Spieler synchronisiert haben:
- Jeder Client sendet die Positionen aller seiner Aufzählungszeichen in jedem Frame.
- Der Server gibt das an jeden Spieler weiter.
Aber es gibt ein Problem.
Sichern gegen Cheats
Wenn Sie das, was der Client Ihnen sendet, als die wahre Position der Kugeln weitergeben, kann ein Spieler betrügen, indem er seinen Client so ändert, dass er Ihnen gefälschte Daten sendet, z. B. Kugeln, die sich dorthin teleportieren, wo sich die anderen Schiffe befinden. Sie können dies ganz einfach selbst ausprobieren, indem Sie die Webseite herunterladen, das JavaScript ändern und erneut ausführen. Dies ist nicht nur ein Problem für Spiele, die für den Browser erstellt wurden. Im Allgemeinen können Sie Daten, die vom Client stammen, nie wirklich vertrauen.
Um dem entgegenzuwirken, versuchen wir ein anderes Schema:
- Der Kunde sendet immer dann, wenn er eine Kugel mit dem Ort und der Richtung abgefeuert hat.
- Der Server simuliert die Bewegung von Kugeln.
- Der Server aktualisiert jeden Client mit dem Speicherort aller Aufzählungszeichen.
- Clients rendern die Aufzählungszeichen an den vom Server empfangenen Speicherorten.
Auf diese Weise ist der Client dafür verantwortlich, wo die Kugel erscheint, aber nicht, wie schnell sie sich bewegt oder wohin sie danach geht. Der Client kann die Position der Aufzählungszeichen in seiner eigenen Ansicht ändern, aber er kann nicht ändern, was andere Clients sehen.
Um dies zu implementieren, füge ich beim Schießen eine Emission hinzu. Ich werde das eigentliche Sprite auch nicht mehr erstellen, da seine Existenz und Position jetzt vollständig vom Server bestimmt wird. Unser neuer Bullet-Shooting-Code in index.html sollte nun folgendermaßen aussehen:
1 |
// Shoot bullet
|
2 |
if(game.input.activePointer.leftButton.isDown && !this.shot){ |
3 |
var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20; |
4 |
var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20; |
5 |
/* The server is now simulating the bullets, clients are just rendering bullet locations, so no need to do this anymore
|
6 |
var bullet = {};
|
7 |
bullet.speed_x = speed_x;
|
8 |
bullet.speed_y = speed_y;
|
9 |
bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,'bullet');
|
10 |
bullet_array.push(bullet);
|
11 |
*/
|
12 |
this.shot = true; |
13 |
// Tell the server we shot a bullet
|
14 |
socket.emit('shoot-bullet',{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y}) |
15 |
}
|
Sie können jetzt auch den gesamten Abschnitt auskommentieren, in dem die Aufzählungszeichen auf dem Client aktualisiert werden:
1 |
/* We're updating the bullets on the server, so we don't need to do this on the client anymore
|
2 |
// Update bullets
|
3 |
for(var i=0;i<bullet_array.length;i++){
|
4 |
var bullet = bullet_array[i];
|
5 |
bullet.sprite.x += bullet.speed_x;
|
6 |
bullet.sprite.y += bullet.speed_y;
|
7 |
// Remove if it goes too far off screen
|
8 |
if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){
|
9 |
bullet.sprite.destroy();
|
10 |
bullet_array.splice(i,1);
|
11 |
i--;
|
12 |
}
|
13 |
}
|
14 |
*/
|
Schließlich müssen wir den Client dazu bringen, auf Bullet-Updates zu warten. Ich habe mich dafür entschieden, dies genauso zu tun wie bei Spielern, bei denen der Server nur ein Array aller Aufzählungsorte in einem Ereignis namens bullets-update
sendet und der Client Aufzählungszeichen erstellt oder zerstört, um die Synchronisierung aufrechtzuerhalten. So sieht das aus:
1 |
// Listen for bullet update events
|
2 |
socket.on('bullets-update',function(server_bullet_array){ |
3 |
// If there's not enough bullets on the client, create them
|
4 |
for(var i=0;i<server_bullet_array.length;i++){ |
5 |
if(bullet_array[i] == undefined){ |
6 |
bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,'bullet'); |
7 |
} else { |
8 |
//Otherwise, just update it!
|
9 |
bullet_array[i].x = server_bullet_array[i].x; |
10 |
bullet_array[i].y = server_bullet_array[i].y; |
11 |
}
|
12 |
}
|
13 |
// Otherwise if there's too many, delete the extra
|
14 |
for(var i=server_bullet_array.length;i<bullet_array.length;i++){ |
15 |
bullet_array[i].destroy(); |
16 |
bullet_array.splice(i,1); |
17 |
i--; |
18 |
}
|
19 |
|
20 |
})
|
Das sollte alles auf dem Client sein. Ich gehe davon aus, dass Sie wissen, wo diese Schnipsel abgelegt werden müssen und wie Sie an dieser Stelle alles zusammensetzen können. Wenn Sie jedoch auf Probleme stoßen, denken Sie daran, dass Sie sich das Endergebnis jederzeit als Referenz ansehen können.
Jetzt müssen wir auf server.js die Aufzählungszeichen verfolgen und simulieren. Zuerst erstellen wir ein Array, um die Kugeln im Auge zu behalten, genauso wie wir eines für die Spieler haben:
1 |
var bullet_array = []; // Keeps track of all the bullets to update them on the server |
Als nächstes hören wir auf unser Shoot Bullet Event:
1 |
// Listen for shoot-bullet events and add it to our bullet array
|
2 |
socket.on('shoot-bullet',function(data){ |
3 |
if(players[socket.id] == undefined) return; |
4 |
var new_bullet = data; |
5 |
data.owner_id = socket.id; // Attach id of the player to the bullet |
6 |
bullet_array.push(new_bullet); |
7 |
});
|
Jetzt simulieren wir die Kugeln 60 Mal pro Sekunde:
1 |
// Update the bullets 60 times per frame and send updates
|
2 |
function ServerGameLoop(){ |
3 |
for(var i=0;i<bullet_array.length;i++){ |
4 |
var bullet = bullet_array[i]; |
5 |
bullet.x += bullet.speed_x; |
6 |
bullet.y += bullet.speed_y; |
7 |
|
8 |
// Remove if it goes too far off screen
|
9 |
if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){ |
10 |
bullet_array.splice(i,1); |
11 |
i--; |
12 |
}
|
13 |
|
14 |
}
|
15 |
|
16 |
}
|
17 |
|
18 |
setInterval(ServerGameLoop, 16); |
Und der letzte Schritt besteht darin, das Update-Ereignis irgendwo innerhalb dieser Funktion zu senden (aber definitiv außerhalb der for-Schleife):
1 |
// Tell everyone where all the bullets are by sending the whole array
|
2 |
io.emit("bullets-update",bullet_array); |
Jetzt können Sie es tatsächlich testen! Wenn alles gut gegangen ist, sollten die Aufzählungszeichen zwischen den Clients korrekt synchronisiert werden. Die Tatsache, dass wir dies auf dem Server getan haben, ist mehr Arbeit, gibt uns aber auch viel mehr Kontrolle. Wenn wir beispielsweise ein Schussgeschoss erhalten, können wir überprüfen, ob die Geschwindigkeit des Geschosses innerhalb eines bestimmten Bereichs liegt. Andernfalls wissen wir, dass dieser Spieler betrügt.
5. Kugelkollision
Dies ist die letzte Kernmechanik, die wir implementieren werden. Hoffentlich haben Sie sich inzwischen daran gewöhnt, unsere Implementierung zu planen und die Client-Implementierung zuerst vollständig abzuschließen, bevor Sie zum Server wechseln (oder umgekehrt). Dies ist eine viel weniger fehleranfällige Methode als das Hin- und Herwechseln bei der Implementierung.
Das Überprüfen auf Kollisionen ist eine wichtige Spielmechanik, daher möchten wir, dass es betrugsfest ist. Wir werden es auf dem Server genauso implementieren, wie wir es für die Aufzählungszeichen getan haben. Wir müssen:
- Überprüfen Sie, ob eine Kugel nahe genug an einem Spieler auf dem Server ist.
- Senden Sie ein Ereignis an alle Kunden, wenn ein bestimmter Spieler getroffen wird.
- Lassen Sie den Kunden das Trefferereignis abhören und das Schiff blinken lassen, wenn es getroffen wird.
Sie können dies ganz alleine versuchen. Um den Player bei einem Treffer zum Blinken zu bringen, setzen Sie einfach das Alpha auf 0:
1 |
player.sprite.alpha = 0; |
Und es wird wieder zu vollem Alpha zurückkehren (dies erfolgt im Player-Update). Für die anderen Spieler würden Sie etwas Ähnliches tun, aber Sie müssen darauf achten, dass ihr Alpha mit so etwas in der Update-Funktion wieder auf eins zurückgesetzt wird:
1 |
for(var id in other_players){ |
2 |
if(other_players[id].alpha < 1){ |
3 |
other_players[id].alpha += (1 - other_players[id].alpha) * 0.16; |
4 |
} else { |
5 |
other_players[id].alpha = 1; |
6 |
}
|
7 |
}
|
Der einzige schwierige Teil, den Sie möglicherweise erledigen müssen, besteht darin, sicherzustellen, dass die eigene Kugel eines Spielers sie nicht treffen kann (andernfalls werden Sie bei jedem Schuss möglicherweise immer mit Ihrer eigenen Kugel getroffen).
Beachten Sie, dass in diesem Schema, selbst wenn ein Client versucht zu betrügen und sich weigert, die vom Server gesendete Treffermeldung zu bestätigen, nur das geändert wird, was er auf seinem eigenen Bildschirm sieht. Alle anderen Spieler werden immer noch sehen, dass dieser Spieler getroffen wurde.
6. Reibungslosere Bewegung
Wenn Sie alle Schritte bis zu diesem Punkt befolgt haben, möchte ich Ihnen gratulieren. Du hast gerade ein funktionierendes Multiplayer-Spiel gemacht! Schicken Sie einen Freund und beobachten Sie die Magie der Online-Multiplayer-Vereinigung von Spielern!
Das Spiel ist voll funktionsfähig, aber unsere Arbeit hört hier nicht auf. Es gibt einige Probleme, die sich auf die Erfahrung des Spielers auswirken können, die wir angehen müssen:
- Die Bewegung anderer Spieler sieht sehr abgehackt aus, es sei denn, jeder hat eine schnelle Verbindung.
- Kugeln können sich nicht mehr ansprechbar anfühlen, da die Kugel nicht sofort abgefeuert wird. Es wartet auf eine Nachricht vom Server, bevor sie auf dem Bildschirm des Clients angezeigt wird.
Wir können das erste Problem beheben, indem wir unsere Positionsdaten für die Schiffe auf dem Client interpolieren. Selbst wenn wir nicht schnell genug Updates erhalten, können wir das Schiff reibungslos dahin bewegen, wo es sein sollte, anstatt es dorthin zu teleportieren.
Die Kugeln erfordern etwas mehr Raffinesse. Wir möchten, dass der Server die Kugeln verwaltet, da dies auf diese Weise betrugsfest ist, aber wir möchten auch das sofortige Feedback erhalten, eine Kugel abzufeuern und sie schießen zu sehen. Der beste Weg ist ein hybrider Ansatz. Sowohl der Server als auch der Client können die Aufzählungszeichen simulieren, wobei der Server weiterhin Aktualisierungen der Aufzählungsposition sendet. Wenn sie nicht synchron sind, nehmen Sie an, dass der Server richtig ist, und überschreiben Sie die Aufzählungsposition des Clients.
Die Implementierung des oben beschriebenen Aufzählungssystems ist für dieses Tutorial nicht zulässig, es ist jedoch gut zu wissen, dass diese Methode vorhanden ist.
Eine einfache Interpolation für die Positionen der Schiffe ist sehr einfach. Anstatt die Position direkt auf dem Aktualisierungsereignis festzulegen, bei dem wir zuerst die neuen Positionsdaten erhalten, speichern wir einfach die Zielposition:
1 |
// Update positions of other players
|
2 |
if(id != socket.id){ |
3 |
other_players[id].target_x = players_data[id].x; // Update target, not actual position, so we can interpolate |
4 |
other_players[id].target_y = players_data[id].y; |
5 |
other_players[id].target_rotation = players_data[id].angle; |
6 |
}
|
Dann durchlaufen wir innerhalb unserer Update-Funktion (noch im Client) alle anderen Spieler und schieben sie auf dieses Ziel zu:
1 |
// Interpolate all players to where they should be
|
2 |
for(var id in other_players){ |
3 |
var p = other_players[id]; |
4 |
if(p.target_x != undefined){ |
5 |
p.x += (p.target_x - p.x) * 0.16; |
6 |
p.y += (p.target_y - p.y) * 0.16; |
7 |
// Interpolate angle while avoiding the positive/negative issue
|
8 |
var angle = p.target_rotation; |
9 |
var dir = (angle - p.rotation) / (Math.PI * 2); |
10 |
dir -= Math.round(dir); |
11 |
dir = dir * Math.PI * 2; |
12 |
p.rotation += dir * 0.16; |
13 |
}
|
14 |
}
|
Auf diese Weise kann Ihr Server Ihnen 30 Mal pro Sekunde Updates senden, aber das Spiel trotzdem mit 60 fps spielen und es wird flüssig aussehen!
Schlussfolgerung
Puh! Wir haben gerade viele Dinge behandelt. Um es noch einmal zusammenzufassen, wir haben uns angesehen, wie Nachrichten zwischen einem Client und einem Server gesendet werden und wie der Status des Spiels synchronisiert wird, indem der Server ihn an alle Spieler weiterleitet. Dies ist der einfachste Weg, um ein Online-Multiplayer-Erlebnis zu schaffen.
Wir haben auch gesehen, wie Sie Ihr Spiel gegen Betrug schützen können, indem Sie die wichtigen Teile auf dem Server simulieren und die Clients über die Ergebnisse informieren. Je weniger Sie Ihrem Kunden vertrauen, desto sicherer wird das Spiel.
Schließlich haben wir gesehen, wie Verzögerungen durch Interpolation auf dem Client überwunden werden können. Die Verzögerungskompensation ist ein weit gefasstes Thema und von entscheidender Bedeutung (einige Spiele werden nur mit einer ausreichend hohen Verzögerung nicht mehr spielbar). Das Interpolieren während des Wartens auf das nächste Update vom Server ist nur eine Möglichkeit, dies zu verringern. Eine andere Möglichkeit besteht darin, die nächsten Frames im Voraus vorherzusagen und zu korrigieren, sobald Sie die tatsächlichen Daten vom Server erhalten. Dies kann jedoch sehr schwierig sein.
Eine völlig andere Art, die Auswirkungen von Verzögerungen zu mildern, besteht darin, nur darum herum zu entwerfen. Der Vorteil, dass sich die Schiffe langsam drehen, um sich zu bewegen, ist sowohl eine einzigartige Bewegungsmechanik als auch eine Möglichkeit, plötzliche Bewegungsänderungen zu verhindern. Selbst mit einer langsamen Verbindung würde dies die Erfahrung nicht ruinieren. Die Berücksichtigung von Verzögerungen bei der Gestaltung der Kernelemente Ihres Spiels kann einen großen Unterschied machen. Manchmal sind die besten Lösungen überhaupt nicht technisch.
Eine letzte nützliche Funktion von Glitch ist, dass Sie Ihr Projekt herunterladen oder exportieren können, indem Sie die erweiterten Einstellungen oben links aufrufen:



Wenn Sie etwas cooles machen, teilen Sie es bitte in den Kommentaren unten! Oder wenn Sie Fragen oder Erläuterungen zu irgendetwas haben, helfe ich Ihnen gerne weiter.