Configurarea unui joc multiplayer cu Socket.io
Partea 1: Introducere
Prerequisites Preferate
În acest tutorial, ne vom concentra pe modul de configurare a unui server și de conectare a jocului tău la acesta, astfel încât să poți activa funcții multiplayer pentru proiectul tău prin comportamente Socket.io. Vom acoperi funcții și comportamente avansate în hyperPad și este foarte recomandat să revizuiești mai întâi câteva tutoriale pentru a avea o bună familiarizare cu software-ul înainte de a continua.
Aceast tutorial presupune că ai o înțelegere bună a funcționalității de bază a hyperPad, precum și o înțelegere minimă a programării scrise, deoarece ne vom concentra pe scriptingul Javascript pentru construirea unui server de joc. Acest tutorial va presupune și o înțelegere de bază a rețelisticii și a diferențelor dintre un server și clienții săi.
Scopul principal al acestui tutorial este să te învețe ce trebuie să faci pentru a adăuga funcționalitate multiplayer în jocul tău; nu pentru a crea jocul în sine. Vom explora relația dintre clienții Socket.io și server și modul în care acestea comunică între ele prin comportamentele Socket.io.
Cerințe
Pentru a crea și a găzdui serverul, vei avea nevoie de un computer capabil să ruleze Node.JS (Mac, Windows, etc). Instalarea Node.JS și configurarea acestuia pentru scopurile noastre vor fi acoperite în acest tutorial. Dacă găzduiești un server în rețeaua ta de acasă, va trebui să schimbi probabil setările de port-forwarding ale routerului dacă dorești să ai conexiuni externe din afara rețelei tale locale.
Pentru acest tutorial, am creat deja un joc demo simplu. În acesta, avem doi jucători care joacă un joc de ștag în labirint. Vom explora cum sunt utilizate comportamentele Socket.io în proiectul de exemplu, așa că se recomandă să descarci și să deschizi proiectul în hyperPad.
Proiectul de exemplu complet hyperPad poate fi descărcat de aici: Multiplayer tag tutorial.tap
Prezentare generală
În acest tutorial, vom trece prin elementele de bază ale creării unui server pentru jocul tău. Serverul va gestiona majoritatea detaliilor din joc, cum ar fi scorurile, camerele de joc etc. Apoi, vom crea comportamente care trimite informații din jocul nostru către server și invers.
Iată fluxul general al jocului nostru:
1. Din meniul principal, ne vom conecta la serverul nostru și vom permite utilizatorului să decidă dacă să creeze sau să se alăture unui joc.
a. Dacă se creează un joc, se va încărca sala de așteptare.
b. Dacă se alătură unui joc, se va încărca o lista de camere de joc disponibile.
c. Când doi jucători sunt în sala de așteptare, jocul se va lansa.
2. Într-un joc, serverul va plasa aleatoriu jucătorii într-una dintre cele patru zone și va asigna unul dintre ei ca "It".
a. Etichetarea celuilalt jucător va schimba locațiile lor, va schimba statutul de It și va adăuga un punct jucătorului care a etichetat celălalt.
b. În cazul în care nu există etichete într-o anumită perioadă de timp, serverul va elimina 1 punct de la jucătorul care este în prezent It și va schimba statutul de It înainte de a plasa din nou jucătorii în zonele de spawn.
3. Odată ce un jucător a atins un anumit număr de puncte, se va încărca o suprapunere declarând un câștigător. Apoi, jucătorii se vor deconecta de la cameră și se vor întoarce la meniul principal.
Partea 2: Configurarea Serverului
Creearea unui server Socket.io necesită să rulăm o aplicație Javascript folosind biblioteca Socket.io, ascultând pentru conexiunile jucătorilor. Socket.io este o bibliotecă de rețelistică Javascript care simplifică multe din detaliile tehnice ale construirii unei aplicații de rețea pentru noi. Mai multe detalii urmează.
Descarcă și Instalează Node.JS
Pentru a începe, du-te la Node.JS și descarcă-l pe computerul (Mac, Windows, etc.) pe care dorești să găzduiești serverul. Când descărcarea s-a terminat, rulează programul de instalare și urmează instrucțiunile. Toate opțiunile în timpul instalării pot fi lăsate pe setările implicite. Node.JS ne oferă capacitatea de a rula aplicații Javascript pe cont propriu fără a avea nevoie de un browser web.
Descărcarea și Rularea Serverului Exemplu
Următorul pas este să descarci exemplul de server multiplayer al acestui tutorial de pe GitHub:
https://github.com/hyperPad/multiplayerServerExample
Apasă pe butonul "Clone or download" și selectează "Download ZIP". Aceasta va descărca o copie a codului pentru exemplul serverului.
Extrage ZIP-ul descărcat. În interior vei găsi câteva fișiere mici, dar cel mai notabil fișier aici este fișierul "index.js" care este codul pentru serverul nostru. Apoi, deschide linia de comandă/terminalul în folderul exemplului serverului.
Tipărește "npm install" și apasă enter, apoi lasă-l să ruleze. npm este un manager de pachete care citește fișierul package.json și descarcă pachetele necesare pentru serverul nostru. (Inclusiv Socket.io!)
Când comanda s-a terminat, avem tot ce ne trebuie pentru a rula serverul. În terminal, scrie "node ." și serverul va porni.
Asta e! Serverul nostru acum ascultă pe portul 3000 pentru conexiuni Socket.io incoming.
"node ." lansează Node.JS pentru directorul curent, unde va căuta fișierul index al directorului și îl va rula. În mod implicit, aceasta este fișierul Javascript "index.js", care conține majoritatea codului serverului nostru și va fi analizat pe parcursul acestui tutorial.
Lăsați terminalul deschis, deoarece închiderea acestuia va închide și serverul. Vei vedea mesaje imprimate atunci când jucătorii se conectează sau se deconectează și când camerele sunt create sau distruse, și alte evenimente.
Notă: Pentru a permite conexiuni incoming din exteriorul unei rețele de acasă, va trebui probabil să deschizi portul 3000 pe gateway-ul tău de acasă. Acest proces variază de la rețea la rețea, dar un ghid poate fi găsit de obicei căutând pe internet un ghid de port-forwarding pentru modemul/routerul de acasă. Este posibil să fie nevoie să închizi și să restartezi serverul Node.JS atunci când schimbi setările de port-forwarding.
Partea 3: Conectarea la Server
Odată ce ai basiscile serverului configurate, acum ne putem conecta la acesta într-un joc hyperPad. Aceasta ar trebui să fie configurată să fie primul lucru care se întâmplă în proiectul tău, indiferent de scena în care te afli (deoarece totul trebuie să comunice cu serverul). Este cel mai bine să atașezi acest comportament unui obiect din Layer Global astfel încât să se aplice în toate scenele.
În proiectul exemplu, eticheta Layer Global "Server" conține cele de mai sus. (Descărcarea pentru proiect se poate găsi în Partea 1 a acestui tutorial în secțiunea Cerințe.)
Aceste două comportamente sunt tot ce ai nevoie pentru a te conecta la server. În primul rând, sub tab-ul personalizat, ia comportamentul Socket.io Client și plasează-l. Aici, vei introduce adresa URL a serverului tău sub tab-ul URL în fereastra sa de proprietăți. În imaginea de mai sus, adresa URL a serverului nostru era "http://192.168.0.191:3000", inclusiv protocolul și portul. Va trebui să schimbi aceasta pentru a se potrivi cu adresa URL a serverului tău sau este posibil să nu funcționeze când pornești jocul.
Acum avem informațiile despre server pe care le dorim, dar trebuie să ne conectăm la acesta. Așadar, tot ce avem nevoie este comportamentul Conectare la Socket. Plasează unul și deschide proprietățile sale, selectează clientul în caseta goală și setează proprietatea Funcție la "Conectare".
Acum, când proiectul nostru se încarcă, ne vom conecta automat la serverul nostru.
Partea 4: Crearea și Alăturarea Camerei
Pasul următor este să creăm sălile noastre de joc în care jucătorii pot intra și juca împreună.
Aici este ecranul nostru simplu de Meniu Principal. Tot ce trebuie să facă cineva este să apese unul dintre butoane pentru a începe o cameră sau a căuta camere disponibile.
Crearea Camerelor
Hai să vedem cum comunicăm cu serverul pentru a crea camera noastră.
Este destul de simplu, nu-i așa? Așadar, pentru a repeta rapid, odată ce atingem butonul nostru, vom fi rugați să introducem un nume pentru cameră. Odată ce asta este finalizată, acel nume este emis către server unde camera este creată și încărcăm scena sălii de așteptare.
De aici înainte, vom folosi adesea Emit la Socket. Asta pentru că este modul nostru principal de a trimite date către server. Poate fi gândit pur și simplu ca "versiunea online" a Mesajului de Difuzare și Mesajului Rece (Emit la Socket efectuează ambele acțiuni simultan).
Acum, trebuie să adăugăm niște informații serverului nostru, astfel încât să creeze camera odată ce primește mesajul din comportamentul nostru Emit.
socket.on('createRoom', (roomName, callback) => {
Această linie creează un eveniment socket (sub forma unei expresii lambda) pe server, ascultând pentru Emits cu evenimentul 'createRoom'.
Primul parametru este valoarea pe care o trimitem prin comportamentul Emit la Socket, în acest caz numele camerei pe care utilizatorul l-a tastat. Aici, am numit acel parametru 'roomName'.
Al doilea parametru este o funcție pe care o apelăm ulterior în evenimentul socket pentru a semnala clientul. Comportamentul Emit la Socket va continua execuția doar dacă funcția callback este apelată pe server. Aici, am numit acel parametru 'callback'.
const room = {
id: uuid(),
name: roomName,
sockets: []
};
Aceasta va crea o instanță a unei structuri care va conține informații esențiale despre o cameră.
'id' va fi un identificator unic generat de funcția utilitară 'uuid()'. Aceasta ne va ajuta să identificăm în mod specific această cameră de o listă de multe alte camere create mai târziu.
'name' va fi setat la numele pe care utilizatorul l-a tastat anterior.
'sockets' va fi inițializat ca un array gol. Mai târziu, acesta va ține evidența socket-urilor jucătorilor conectați la acea cameră.
rooms[room.id] = room;
'rooms' este o listă globală de camere care sunt în prezent active. Deoarece creăm o cameră nouă, o vom stoca în listă după ID-ul său.
joinRoom(socket, room);
Aceasta va apela funcția globală 'joinRoom' (linia 29), care va adăuga socket-ul jucătorului la array-ul 'sockets' al camerei. Deoarece jucătorul a creat camera, acest lucru îi va permite și să se alăture.
callback();
});
În cele din urmă, invocăm funcția callback pentru a notifica clientul că camera a fost creată. Aceasta va permite comportamentului Emit la Socket să continue execuția, ceea ce va încărca scena Sălii de așteptare următoare. Aceasta marchează, de asemenea, sfârșitul evenimentului socket 'createRoom'.
Alăturarea Camerelor
Alăturarea camerelor este puțin diferită deoarece va trebui să accesăm informațiile dintr-o scenă diferită din buton.
Pentru a alătura camere, am plasat începutul comportamentelor noastre pe layerul global, împreună cu unde ne conectăm la server. În acest fel, putem accesa informațiile din oricare dintre scenele noastre, în acest caz lista noastră de camere. Pentru buton, atingerea acestuia va încărca pur și simplu jucătorul în scena listei de camere.
Aici avem comportamentul nostru secundar pentru comunicarea cu serverul. Evenimentul Socket poate fi gândit ca un Mesaj Rece, deoarece se va activa doar o dată ce mesajul setat a fost difuzat de la server.
Cel mai bun mod de a gândi atunci când folosim Evenimentul Socket și Emit la Socket este că Evenimentul Socket reacționează doar la informațiile venite de la server, în timp ce Emit la Socket este apelat ca reacție la lucruri făcute local.
În ceea ce privește logica noastră, odată ce ne conectăm la server, vom Emite cererea 'getRoomNames' pentru a obține orice nume de camere disponibile.
Apoi, am seta o etichetă pe care jucătorul poate apăsa pentru a intra.
Aceasta este scena noastră Lista Camerelor. Aici vor fi încărcate și afișate orice camere de joc disponibile. Pur și simplu apăsând pe numele camerei se va încărca jucătorul. Grație comportamentelor noastre anterioare, lista se va încărca automat cu orice camere deschise și va genera etichetele numelui camerelor pe ecran. Dacă nu apare nimic, avem butonul nostru Reîmprospătați Lista, care pur și simplu repetă comportamentele pentru a le încărca încă o dată.
Aici avem logica pentru configurarea listei. Nu vom intra în prea multe detalii aici, pentru că doar vrem să știm cum se conectează aceasta la serverul nostru.
Pentru a începe, avem comportamentul Emit la Socket. Acesta solicită serverului să trimită informații cu privire la camerele disponibile. De acolo, avem un comportament Obține Valoare Array. Toate datele care vin de la server vor fi trimise ca un Array, iar informațiile necesare vor fi în primele valori. Așadar, ne setăm Obține Valoare Array pentru a obține valoarea de la indexul 0. De acolo, comportamentele noastre vor extrage acele date și vor crea o etichetă pentru fiecare cameră, generându-le pe ecranul nostru.
Apoi, ne vom verifica obiectul pe care este nevoie să apăsăm, în scena noastră este eticheta numită Nume Cameră.
Acest text acționează ca butonul nostru atunci când este generat, dar trebuie să atașăm încă ID-ul camerei pe care dorim să ne conectăm. Pentru a face asta, trebuie mai întâi să obținem ID-ul camerei, iar acest lucru îl realizăm cu comportamentul Obține Atribut și îl setăm ca dinamic. Apoi, vom emite către server că vrem să ne alăturăm acestei camere și să ne încărcăm în sala de așteptare.
socket.on('joinRoom', (roomId, callback) => {
Aceasta este intrarea pentru evenimentul socket 'joinRoom'. Valoarea primului parametru va fi ID-ul camerei la care dorim să ne alăturăm care a fost emis la server. Aici, am numit parametrul 'roomId'.
const room = rooms[roomId];
joinRoom(socket, room);
Folosind roomId dat de client, putem găsi instanța corespunzătoare a camerei pe server. Cu aceasta, invocăm funcția globală 'joinRoom' (linia 29) cu socket-ul jucătorului care vrea să se conecteze și instanța camerei. Vom analiza funcția 'joinRoom' în curând.
callback();
});
În cele din urmă, invocăm funcția callback pentru a notifica clientul să continue încărcarea scenei Sălii de așteptare, marcând sfârșitul evenimentului socket 'joinRoom'.
Așadar, ce se întâmplă exact în funcția 'joinRoom'? Să aruncăm o privire.
room.socket.push(socket);
După cum s-a menționat anterior, array-ul de membri 'room.socket' ține evidența socket-urilor conectate într-o cameră. Această linie face exact asta, împingând socket-ul în array.
socket.join(room.id, () => {
socket.roomId = room.id;
console.log(socket.id, "Joined", room.id);
});
Aceasta este apelul oficial pentru a conecta clientul la o cameră. În primul rând, spunem socket-ului să se alăture unei camere prin ID-ul său. Când acest lucru este complet, următoarea funcție callback este invocată, unde atașăm ID-ul camerei la socket. În cele din urmă, ne logăm în consolă că un jucător s-a alăturat unei camere!
Așteptând în Cameră
Camera de așteptare este pur și simplu o scenă în care îi încărcăm pe jucători în timp ce așteaptă ca alți jucători să se alăture sau ca un joc să înceapă.
La intrarea în cameră, vom emite un mesaj 'ready' către server. Odată ce serverul a primit două astfel de mesaje, va trimite un mesaj că începe jocul ('initGame'). Prindem acel mesaj cu Evenimentul nostru Socket și astfel, ne încărcăm nivelul jocului. Pentru comportamentele noastre, cam atât. Poți adăuga un buton care te va deconecta din cameră și te va trimite înapoi la meniul principal dacă dorești.
Pe partea serverului, hai să analizăm codul pentru a vedea ce se întâmplă acolo.
Aceasta este evenimentul 'ready' care este apelat când un client s-a alăturat unei camere și este gata să se conecteze.
const room = rooms[socket.roomId];
Deoarece am atașat ID-ul camerei la socket, suntem în măsură să obținem camera pentru a verifica dacă jocul poate începe.
if (room.sockets.length == 2) {
Aici verificăm dacă acum sunt doi jucători care așteaptă în cameră. Să zicem că acest lucru este adevărat acum, și continuăm să începem jocul.
for (const client of room.sockets) {
client.emit('initGame');
}
Acum că sunt doi jucători, iterăm prin fiecare socket și emitem evenimentul 'initGame' astfel încât fiecare client să încarce scena Nivel, așa cum s-a arătat mai devreme.
Partea 5: Gameplay
Acum, să intrăm în miezul lucrurilor. Aici este nivelul nostru de joc pe care l-am conceput pentru acest tutorial.
Înainte de a ajunge la asta, totuși, avem o etichetă intitulată "Logica Jocului", haideți să o deschidem și să aruncăm o privire.
Wow, asta este o mulțime de comportamente! Nu te îngrijora, totuși, aceasta este pur și simplu modul de a genera jucătorii noștri în joc. Să ne uităm mai îndeaproape;
Începem cu un comportament Emit la Socket care spune serverului că jocul nostru a început, cu evenimentul 'startGame'. Apoi, luăm array-ul pe care serverul îl returnează, luăm prima valoare a acestuia și cu Obține Valoare Dicționar obținem diversele atribute de care obiectul nostru va avea nevoie. O altă structură face la fel, exceptând jucătorul adversar. În cele din urmă, difuzăm mesajul 'init' obiectului nostru jucător pentru a da startul.
Să vedem ce se întâmplă pe partea serverului atunci când emitem evenimentul 'startGame'.
Prima jumătate a acestui eveniment socket pe care o vezi aici setează unele valori inițiale pentru fiecare client, apoi adaugă orice client care nu este clientul emitent în array-ul local 'others'.
În a doua jumătate, o listă de dicționare locale numită 'ack' este creată. În acestea, avem informații despre sine și ceilalți clienți. Apoi, trimitem aceste informații înapoi clientului prin trecerea dicționarului nostru 'ack' la funcția de callback, care devine valoarea rezultată a comportamentului Emit la Socket care a fost apelat.
Ulterior, se face un apel de timeout de 5 secunde pentru a începe efectiv runda, acum că toată lumea are toate informațiile de care au nevoie pentru a juca jocul. Funcția 'beginRound' (linia 99) controlează logica specifică jocului pentru acest proiect. Nu vom intra în prea multe detalii despre această parte, dar esențial controlează unde să genereze jucătorii, verificarea scorurilor, precum și informarea clienților cine este It.
După cum am menționat anterior, mesajul 'init' este apelat pe eticheta noastră "Logica Jocului" când totul este gata. În obiectul jucătorului, acum ne vom uita la comportamentele acestuia unde primește mesajul 'init'.
Aici poți vedea că avem mai multe structuri comportamentale pe jucătorul nostru. În primul rând, vom începe cu structura din stânga sus.
Aceasta este comportamentul care practic începe tot restul din jocul nostru.
În primul rând, primim mesajul 'init' trimis de "Logica Jocului". De acolo, obținem ID-ul serverului obiectului nostru și activăm un comportament de Socket Events și, de asemenea, configurăm ecranul jocului astfel încât să putem urmări corect personajul nostru.
Sincronizarea Mișcării
Aici este unul dintre cele mai importante comportamente ale noastre. Această mică structură are rolul de a actualiza poziția jucătorului nostru pe server de fiecare dată când mișcăm joystick-ul. Probabil ai observat că face referire la unele valori de dicționar. Obținem acele valori dintr-un comportament de dicționar standalone care conține pozițiile X și Y ale jucătorului nostru.
Haideți să aruncăm o privire la evenimentul 'moved' pe server.
data = JSON.parse(data);
Dictionarele, atunci când sunt trimise de la un client la un server, trebuie să fie analizate pentru a citi datele ușor. Acest lucru se datorează faptului că dicționarele în hyperPad sunt codificate într-o structură JSON atunci când sunt emise către un server. Această linie analizează structura JSON și stochează lista dicționarului înapoi în aceeași variabilă locală 'data'.
socket.x = data.x;
socket.y = data.y;
Aici, actualizăm pozițiile X și Y stocate pe socket cu noile valori date de client.
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
});
}
Apoi, iterăm prin toți clienții pentru a-i actualiza cu privire la noua noastră poziție și alte detalii, excluzând pe noi înșine (deci, clientul emitent nu are nevoie să știe propria poziție).
Această structură controlează majoritatea jocului nostru. Folosim un Eveniment Socket când serverul verifică care jucător este etichetat ca It, care este emis de 'beginRound' (linia 99) pe server.
Apoi, ne obținem ID-ul de server al obiectului din array și folosim Valoarea Dicționarului pentru a-l descompune în diferitele piese de date pe care le conține. De acolo, ne obținem Scorul, dacă suntem etichetați ca It, precum și pozițiile x și y ale obiectului nostru, apoi le aplicăm la atributele obiectului. Restul comportamentelor sunt pentru configurarea și controlul UI-ului din jocul nostru.
Concluzie
A fost mult de absorbit aici, dar sperăm că dacă ai ajuns până aici, ar trebui să ai o înțelegere despre cum să utilizezi comportamentele Socket.io pentru a crea experiențe multiplayer pentru jucătorii tăi.
Încearcă singur! Ia un joc existent pe care l-ai creat și încearcă să-i oferi unele funcționalități online, cum ar fi o scenă leaderboard de scoruri care se conectează la un server și solicită o listă cu primele 10 scoruri cu numele jucătorului pentru a le afișa.
Este greu să predai un limbaj de scripting precum Javascript într-un singur articol. Din fericire, dacă ai probleme, există o groază de alte resurse pentru a te ajuta să scrii aplicații Javascript pentru Node.JS și Socket.io;
Învăță Javascript - https://developer.mozilla.org/bm/docs/Web/JavaScript
Învăță Socket.io - https://socket.io/docs/
Învăță Node.JS - https://nodejs.org/en/docs/

