Skapa ett flerspelarspel med Socket.io
Del 1: Introduktion
Preferred Prerequisites
I den här handledningen kommer vi att fokusera på hur man ställer in en server och kopplar ditt spel till den så att du kan aktivera flerspelarfunktioner för ditt projekt via Socket.io-beteenden. Vi kommer att gå igenom avancerade funktioner och beteenden i hyperPad och det är starkt rekommenderat att först granska några andra handledningar och ha en bra känsla för programvaran innan du fortsätter.
Denna handledning förutsätter att du har en bra förståelse för hyperPads kärnfunktionalitet samt en minimal förståelse för skriven programmering, eftersom vi kommer att fokusera på Javascript-skript för att bygga en spels server. Denna handledning kommer också att förutsätta en viss grundläggande förståelse för nätverk och skillnaderna mellan en server och dess klienter.
Det primära målet med denna handledning är att lära dig vad du behöver göra för att lägga till flerspelarfunktionalitet i ditt spel; inte att skapa själva spelet. Vi kommer att utforska relationen mellan Socket.io-klienter och servern, och hur de kommunicerar med varandra med Socket.io-beteenden.
Krav
För att skapa och värd servern behöver du en dator som kan köra Node.JS (Mac, Windows, etc). Installationen av Node.JS och hur man ställer in det för vårt syfte kommer att täckas i denna handledning. Om du värdar en server i ditt hemnätverk måste du troligen ändra din gateways portforward-inställningar om du vill ha inkommande anslutningar från utanför ditt lokala nätverk.
För denna handledning har vi redan skapat ett enkelt demospel. I det har vi två spelare som spelar ett spel av tag i en liten labyrint. Vi kommer att utforska hur Socket.io-beteenden används i exempelprojektet, så det rekommenderas att du laddar ner och öppnar projektet i hyperPad.
Det kompletta hyperPad-exempelprojektet kan laddas ner här: Multiplayer tag tutorial.tap
Översikt
I denna handledning kommer vi gå igenom grunderna i att skapa en server för ditt spel. Servern kommer att hantera de flesta detaljerna i spelet såsom poängsättning, spel-lobbys, osv. Sedan kommer vi att skapa beteenden som skickar information från vårt spel till servern och vice versa.
Här är det allmänna flödet för vårt spel:
1. Från huvudmenyn kommer vi att koppla till vår server och låta användaren besluta om att skapa eller gå med i ett spel.
a. Om du skapar ett spel, ladda väntelokalen.
b. Om du går med i ett spel, ladda en lista med tillgängliga spelrum.
c. När två spelare är i en väntelokal, så startar spelet.
2. I ett spel placerar servern slumpmässigt spelarna i en av fyra områden och tilldelar en av dem att vara "It".
a. Att tagga den andra spelaren kommer att randomisera deras platser, byta "It" status och lägga till en poäng till spelaren som taggade den andra.
b. Om inga taggar inträffar inom en viss tid, kommer servern att ta bort 1 poäng från spelaren som för närvarande är "It" och byta "It" status innan den slumpmässigt placerar spelarna i spawn-zonerna igen.
3. När en spelare har nått ett visst antal poäng, kommer en overlay att laddas som deklarerar en vinnare. Därefter kommer spelarna att koppla från rummet och återvända till huvudmenyn.
Del 2: Ställa in servern
Att skapa en Socket.io-server kräver att vi kör en Javascript-applikation med hjälp av Socket.io-biblioteket, där vi lyssnar på spelaranslutningar. Socket.io är ett Javascript-nätverksbibliotek som förenklar mycket av underarbetet i att bygga en nätverksansluten applikation för oss. Mer detaljer följer.
Hämta och installera Node.JS
För att komma igång, gå till Node.JS och ladda ner det till den dator (Mac, Windows, etc.) som du vill värda servern på. När nedladdningen är klar, kör installationsprogrammet och följ dess instruktioner. Alla alternativ under installationen kan lämnas som standard. Node.JS ger oss möjligheten att köra Javascript-applikationer på egen hand utan behov av en webbläsare.
Hämta och köra exempelservern
Nästa steg är att ladda ner den här handledningens Multiplayer Server Example från GitHub:
https://github.com/hyperPad/multiplayerServerExample
Klicka på knappen "Klona eller ladda ner" och välj "Ladda ner ZIP". Detta kommer att ladda ner en kopia av koden för serverexemplet.
Extrahera den nedladdade ZIP-filen. Inuti hittar du några små filer, men den mest anmärkningsvärda filen här är "index.js"-filen som är koden för vår server. Nästa steg är att öppna din kommandorad/terminal inne i serverexempelfoldern.
Skriv "npm install" och tryck på enter, låt sedan det köras. npm är en paketförvaltare som läser package.json-filen och laddar ner de nödvändiga paketen för vår server. (Inklusive Socket.io!)
När kommandot är färdigt har vi allt vi behöver för att köra servern. I terminalen, skriv "node ." och servern kommer att starta.
Det är allt! Vår server lyssnar nu på port 3000 för inkommande Socket.io-anslutningar.
"node ." startar Node.JS för den aktuella katalogen, där den kommer att leta efter katalogindexfilen och köra den. Som standard är detta "index.js" Javascript-filen, där bulk av vår servers kod ligger och kommer att analyseras genom denna handledning.
Lämna terminalen öppen, eftersom stängning av den också kommer att stänga servern. Du kommer att se meddelanden skrivas ut när spelare ansluter eller kopplar ifrån, och när rum skapas eller raderas och andra händelser.
Observera: För att tillåta inkommande anslutningar från utanför ett hemnätverk måste du troligen öppna port 3000 på din hemgateway. Denna process varierar från nätverk till nätverk, men en guide kan vanligtvis hittas genom att göra en internetsökning efter en portforwarding-guide för din hemmodem/router. Du kan behöva stänga av och starta om Node.JS-servern när du ändrar portforwarding-inställningar.
Del 3: Koppla till servern
När du väl har grunderna för servern startade, kan vi nu koppla till den i ett hyperPad-spel. Detta bör ställas in så att det är det första som händer i ditt projekt oavsett vilket scen du befinner dig på (eftersom allt måste kommunicera med servern). Det är bäst att koppla detta beteende till ett objekt på den globala lagret så det tillämpas över alla scener.
I exempelprojektet innehåller den globala lagret "Server"-etiketten ovan. (Nedladdningen av projektet kan hittas i Del 1 av den här handledningen under Krav-sektionen.)
Dessa två beteenden är verkligen allt du behöver för att koppla till servern. Först, under fliken för anpassade beteenden, ta tag i Socket.io-klientbeteendet och släpp ner det. Här kommer du att ange serverns URL under URL-fliken i egenskapsfönstret. I bilden ovan var vår servers URL "http://192.168.0.191:3000", inklusive protokoll och port. Du kommer att vilja ändra detta för att matcha din server-URL så att det förmodligen fungerar när du startar spelet.
Nu har vi serverinformationen vi vill ha, men vi måste fortfarande koppla till den. Så allt vi behöver är "Koppla till Socket"-beteendet. Släpp ner ett, öppna dess egenskaper och välj klienten i den tomma rutan och ställ in Funktions-egenskapen till "Anslut".
Nu, när vårt projekt laddas, kommer vi automatiskt att koppla till vår server.
Del 4: Skapa och gå med i rum
Det nästa steget är att skapa våra spel-lobbys där spelare kan gå med och spela tillsammans.
Här är vår enkla huvudmenyskärm. Allt du behöver göra är att trycka på en av knapparna för att antingen starta ett rum eller leta efter tillgängliga rum.
Skapa rum
Låt oss kolla hur vi går tillväga för att kommunicera med servern för att skapa vårt rum.
Relativt enkelt eller hur? Så för att snabbt gå igenom detta, när vi rör vid vår knapp kommer vi att bli ombedda att skriva in ett rumnamn. När det är klart skickas det namnet till servern där rummet skapas och vi laddar upp väntelokalens scen.
Här framöver kommer vi att använda "Emit to Socket" ofta. Det beror på att det är vårt huvudsakliga sätt att skicka data till servern. Tänk på det helt enkelt som den "online-versionen" av "Broadcast Message" och "Receive Message" ("Emit to Socket" utför båda handlingarna på en gång).
Nu behöver vi lägga till lite information till vår server så att den ska skapa rummet när den får meddelandet från vårt "Emit"-beteende.
socket.on('createRoom', (roomName, callback) => {
Denna rad skapar en socket-händelse (som en lambda-uttryck) på servern, som lyssnar efter Emit:er med händelsen 'createRoom'.
Den första parametern är värdet vi skickar genom Emit to Socket-beteendet, i det här fallet rumnamn som användaren skrev in. Här har vi namngivit den parametern 'roomName'.
Den andra parametern är en funktion som vi anropar senare i sockethändelsen för att signalera klienten. Emit to Socket-beteendet kommer endast att fortsätta utförande om callback-funktionen anropas på servern. Här har vi namngivit den parametern 'callback'.
const room = {
id: uuid(),
name: roomName,
sockets: []
};
Detta kommer att skapa en instans av en struktur som kommer att innehålla nödvändig information om ett rum.
'id' kommer att vara en unik identifierare som genereras av hjälp-funktionen 'uuid()'. Detta kommer att hjälpa oss att identifiera just detta rum bland en lista av många andra skapade rum senare.
'name' kommer att sättas till det namn som användaren skrev in tidigare.
'sockets' kommer att initialiseras som en tom array. Senare kommer detta att hålla reda på spelarsocklar som är anslutna till det rummet.
rooms[room.id] = room;
'rooms' är en global lista över rum som för närvarande är aktiva. Eftersom vi skapar ett nytt rum, kommer vi att lagra det i listan med dess ID.
joinRoom(socket, room);
Detta kommer att anropa den globala funktionen 'joinRoom' (rad 29), som kommer att lägga till spelarens socket i rummets 'sockets'-array. Eftersom spelaren skapade rummet, kommer detta också att göra att de går med i rummet också.
callback();
});
Slutligen anropar vi callback-funktionen så att klienten får meddelande att rummet har skapats. Detta gör att "Emit to Socket" kan fortsätta exekveringen, vilket laddar upp väntelokalens scen nästa. Detta markerar också slutet av 'createRoom'-sockethändelsen.
Gå med i rum
Att gå med i rum är lite annorlunda då vi kommer att behöva komma åt informationen från en annan scen än knappen.
För att gå med i rum har vi placerat början av våra beteenden på vårt globala lager tillsammans med där vi kopplar till servern. På så sätt kan vi komma åt informationen från någon av våra scener, i det här fallet vår rumslista. För knappen kommer det att helt enkelt ladda in spelaren i rumslistescenen.
Här har vi vårt andra beteende för att kommunicera med servern. Socket Event kan tänkas som "Receive Message" eftersom den endast kommer att aktiveras när det angivna meddelandet har sänds från servern.
Det bästa att tänka på när man använder Socket Event och Emit to Socket är att Socket Event reagerar endast på information som kommer från servern, medan Emit to Socket kallas som reaktion på saker som görs lokalt.
I vårt logik, när vi ansluter till servern kommer vi att Emit "getRoomNames"-begäran för att få eventuella tillgängliga rumsnamn.
Därefter skulle vi ställa in en etikett som spelaren kan trycka på för att komma in.
Detta är vår rumslista-scen. Här kommer alla tillgängliga spelsrum att laddas och visas. Genom att helt enkelt trycka på rumsnamnet kommer spelaren att lastas in. Tack vare våra tidigare beteenden kommer listan automatiskt att ladda alla öppna rum och spawna rumsnamnetiketter på skärmen. Om ingenting visas har vi vår Refresh List-knapp som helt enkelt upprepar beteendena för att ladda dem en gång till.
Här har vi logiken för att ställa in listan. Vi kommer inte att gå in på för mycket detaljer här eftersom vi bara vill veta hur detta kopplar ihop med vår server.
För att börja har vi vårt Emit to Socket-beteende. Detta anropar servern för att skicka information om tillgängliga rum. Från och med där, har vi en Get Array Value-beteende. All data som kommer från servern kommer att skickas som ett Array och den information som behövs kommer att vara på de första värdena. Så vi ställer in vår Get Array Value för att få värdet vid index 0. Från och med där kommer våra beteenden att extrahera den datan och skapa en etikett för varje rum, som spawna dem på vår skärm.
Nästa steg är att kontrollera objektet som vi behöver trycka på, i vår scen är det etiketten med namnet Rumsnamn.
Denna text agerar som vår knapp när den spawnas in, men vi behöver fortfarande koppla rums-ID:t som vi vill ansluta till. För att göra det, behöver vi först få rums-ID:t, och vi gör det med Get Attribute-beteendet och sätter det på dynamiskt. Sedan Emit till servern att vi vill gå med i detta rum och ladda in i väntelokalen.
socket.on('joinRoom', (roomId, callback) => {
Detta är inträdespunkten för sockethändelsen 'joinRoom'. Den första parametern värdet skulle vara det rums-ID vi vill ansluta till som skickades till servern. Här namngav vi parametern 'roomId'.
const room = rooms[roomId];
joinRoom(socket, room);
Med hjälp av roomId som ges av klienten kan vi hitta rätt ruminstans på servern. Med det anropar vi den globala funktionen 'joinRoom' (rad 29) med spelarens socket som vill koppla, och ruminstansen själv. Vi kommer att ta en titt på 'joinRoom'-funktionen om en liten stund.
callback();
});
Slutligen anropar vi callback-funktionen för att meddela klienten att fortsätta ladda upp väntelokalens scen, vilket markerar slutet av 'joinRoom'-sockethändelsen.
Så, vad exakt händer i 'joinRoom'-funktionen? Låt oss ta en titt.
room.socket.push(socket);
Som nämnts tidigare, så håller 'room.socket'-medlemsarrayen reda på de sockets som är anslutna i ett rum. Denna rad gör precis det genom att trycka socken in i arrayen.
socket.join(room.id, () => {
socket.roomId = room.id;
console.log(socket.id, "Gick med i", room.id);
});
Detta är det officiella anropet för att koppla klienten till ett rum. Först berättar vi för socken att gå med i ett rum med dess ID. När det är klart anropas följande callback, där vi kopplar rums-ID:t till socken. Sist men inte minst, loggar vi i konsolen att en spelare har gått med i ett rum!
Väntelokalen
Väntelokalen är helt enkelt en scen som vi laddar spelarna in i medan de väntar på att andra spelare ska gå med, eller att ett spel ska börja.
Vid inträdet i rummet kommer vi att Emit en "ready"-meddelande till servern. När servern har tagit emot två av dessa kommer den att skicka ett meddelande som meddelar att spelet startar ('initGame'). Vi plockar upp detta meddelande med vår Socket Event och laddar därmed upp vår spelnivå. För våra beteenden är det mesta av det. Du kan lägga till en knapp som kopplar ifrån dig från rummet och skickar dig tillbaka till huvudmenyn om du vill.
På serversidan, låt oss analysera koden för att se vad som händer där.
Detta är 'ready'-händelsen som anropas när en klient har gått med i ett rum och är redo att koppla.
const room = rooms[socket.roomId];
Eftersom vi har kopplat rums-ID:t till socken kan vi få rummet för att kontrollera om spelet kan starta.
if (room.sockets.length == 2) {
Här kontrollerar vi om det nu finns två spelare som väntar i rummet. Låt oss säga att det nu är sant, och vi fortsätter för att starta spelet.
for (const client of room.sockets) {
client.emit('initGame');
}
Nu när det finns två spelare, itererar vi igenom varje socket och emitterar 'initGame'-evenemanget så att varje klient laddar nivåscenen, precis som nämnts tidigare.
Del 5: Spelande
Nu, för att komma till köttet i saken. Detta är där 90% av vårt arbete kommer att gå. Nedan ser du vår spelnivå som vi designade för denna handledning.
Innan vi kommer till det, har vi en etikett med namnet "Spel Logik", låt oss öppna den och ta en titt.
Wow, det är många beteenden! Oroa dig inte, det här är helt enkelt hur vi spawner våra spelare i spelet. Låt oss ta en närmare titt;
Vi börjar med en Emit to Socket-beteende som meddelar servern att vårt spel har startat, med evenemanget 'startGame'. Vi hämtar sedan arrayen som servern returnerar, tar dess första värde och med hjälp av Get Dictionary Value får vi de olika attributen som vårt objekt behöver. Ett separat träd gör samma sak, med undantag för motståndarspelet. Sist sänder vi meddelandet 'init' till vår spelobjekt för att få saker att börja.
Låt oss se vad som händer på serversidan när vi emitterar 'startGame'-evenemanget.
Den första halvan av denna sockethändelse ställer in några initiala värden för varje klient, och lägger sedan till varje klient som inte är den emitterande klienten i den lokala 'others'-arrayen.
I den andra halvan skapas en lokal ordboksliste som kallas 'ack'. I den har vi information om oss själva och de andra klienterna. Vi skickar sedan den informationen tillbaka till klienten genom att skicka vår ordbok 'ack' till callback-funktionen, vilket blir det resulterande värdet av den kalla Emit to Socket-beteendet.
Efteråt görs en timeout-anrop för 5 sekunder för att äntligen starta rundan, nu när alla har all info de behöver för att spela spelet. 'beginRound'-funktionen (rad 99) hanterar viss spel specifik logik för detta projekt. Vi kommer inte att gå in på för mycket detalj kring det, men i grund och botten hanterar det var de ska spawna spelarna, kontrollerar poäng och berättar för klienterna vem som är "It".
Som nämnts tidigare kallas "init"-meddelandet på vår "Spel Logik"-etikett när allt är klart. I spelobjektet kommer vi nu att se på beteendena på det där det får "init"-meddelandet.
Här kan vi se att vi har flera beteendetträd på vår spelare. Först, kommer vi att börja med det övre vänstra trädet.
Detta är beteendet som praktiskt taget startar upp allt annat i vårt spel.
Först får vi "init"-meddelandet som skickas från "Spel Logik"-etiketten. Därefter hämtar vi server-ID:t för vårt objekt och slår på ett av våra Socket Events samt ställer in vår spelskärm så att vi kan följa vår karaktär exakt.
Rörelsesynkronisering
Här är ett av våra viktigaste beteenden. Detta lilla träd är utformat för att uppdatera vår spelares position på servern varje gång vi rör joystick. Du har troligen märkt att det också refererar till några ordboks värden. Vi får dessa värden från ett fristående ordboksbeteende som innehåller X- och Y-positionerna för vår spelare.
Låt oss ta en titt på "moved"-evenemanget på servern.
data = JSON.parse(data);
Ordböcker, när de skickas från en klient till en server, måste analyseras för att läsa datan enkelt. Detta beror på att ordböcker i hyperPad är kodade i en JSON-struktur när de sänds till en server. Denna rad analyserar JSON-strängstrukturen och lagrar ordbokslistan tillbaka i samma lokala 'data'-variabel.
socket.x = data.x;
socket.y = data.y;
Här uppdaterar vi X- och Y-positionerna som lagras på socken med de nya värdena som ges av klienten.
for (const client of room.sockets) {
if (client == socket) {
continue;
}
client.emit(socket.id, {
x: socket.x,
y: socket.y,
score: socket.score,
isIt: socket.isIt
});
}
Nästa steg itererar vi över alla klienter för att informera dem om vår nya position och andra detaljer, med undantag för oss själva (dvs. Den emitterande klienten behöver inte veta sin egen position).
Detta träd här kontrollerar huvuddelen av vårt spel. Vi använder ett Socket Event när servern kontrollerar vilken spelare som är taggad som "It", vilket emitteras av 'beginRound' (rad 99) på servern.
Därefter hämtar vi vårt objekts server-ID från arrayen och använder Dictionary Value för att bryta ner det i de olika datadetaljerna som det innehåller. Därifrån hämtar vi vår poäng, om vi är taggad som "It", såväl som x- och y-positionerna för vårt objekt och applicerar dem på objektets attribut. Resterande beteenden är för att ställa in och kontrollera UI i vårt spel.
Slutsats
Det var mycket att ta in här, men förhoppningsvis, om du har kommit så här långt, bör du ha en förståelse för hur man utnyttjar Socket.io-beteenden för att skapa flerspelarupplevelser för dina spelare.
Prova det själv! Ta ett befintligt spel du har skapat och försök ge det någon online-funktionalitet, såsom en highscore-lista som kopplar till en server och begär en lista över de 10 bästa poängen med spelarnamn att visa.
Det är svårt att lära ut ett skriptspråk som Javascript i en enskild artikel. Lyckligtvis, om du har problem finns det massor av andra resurser för att hjälpa dig med att skriva Javascript-applikationer för Node.JS och Socket.io;
Lär dig Javascript - https://developer.mozilla.org/bm/docs/Web/JavaScript
Lär dig Socket.io - https://socket.io/docs/
Lär dig Node.JS - https://nodejs.org/en/docs/

