În primele articole am prezentat afișarea paletelor cât și verificarea coliziunilor pentru paletele din jocul nostru. Continuăm cu controlul paletelor folosind balansarea telefonului și vom implementa capacitatea de multiplayer folosind WebSocketuri.
Vom realiza o corelare directă între gradul de înclinare a telefonului și mișcarea paletei pe ecran. Se va folosi doar componenta beta, axa X, din cele trei componente primite de la eveniment. În codul de mai jos se poate vedea formula de calcul a noii poziții:
let handler = (event) => {
console.log(
"Alpha (z-axis):",
event.alpha
);
console.log(
"Beta (x-axis):",
event.beta
);
console.log(
"Gamma (y-axis):",
event.gamma
);
beta.textContent =
event.beta;
let newPos =
(event.beta - 20) * 5;
gamma.textContent =
newPos;
if (newPos > 0 &&
newPos < 500) {
this.setBallY(newPos);
}
};
Dar, bineînțeles, partea cea mai dificilă rămâne integrarea iOS/Android și obținerea permisiunilor. Un amănunt important este că pagina web trebuie să aibă o adresă reală și sigură (https
).
try {
if (DeviceMotionEvent &&
typeof DeviceMotionEvent
.requestPermission ===
"function") {
// iOS 13+
let resp = DeviceMotionEvent
.requestPermission()
.then(permissionState => {
console.log(
"got permission state:",
permissionState
);
if (permissionState ===
"granted") {
window.addEventListener(
"devicemotion",
handler
);
window.addEventListener(
"deviceorientation",
handler
);
}
})
.catch(error => {
console.error(error);
});
} else {
// Android or older iOS
window.addEventListener(
"devicemotion",
handler
);
window.addEventListener(
"deviceorientation",
handler
);
console.log(
"no permission is possible!"
);
}
} catch (e) {
console.error(
"unable to init gestures:",
e
);
}
Desenarea paletei se realiza până acum în funcție de numărul de frame-uri generate pe secundă. În cazul nostru avem 60/sec. Redesenarea de fiecare dată a elementelor grafice poate ridica o problemă de performanță. O soluție simplă este să creăm un dirty flag pe care să îl setăm (true) de fiecare dată când avem nevoie de o redesenare. Astfel metoda draw()
devine:
draw(){
if ( this.#isDirty) {
this.ctx.beginPath();
this.ctx.fillStyle = "yellow";
this.ctx.fillRect(this.x - 20,
0, this.w + 40, 600);
this.ctx.fillStyle =
this.#usedColor;
this.ctx.fillRect(this.x,
this.y, this.w, this.h);
this.#isDirty=false;
if (!this.#isRemote) {
this.#isRemote=false;
return this.y;
}
}
}
setBallY(y){
this.y=y;
this.#isDirty=true;
}
Setarea dirty flagului se poate vedea în metoda de mai sus. Orice modificare a poziției paletei aduce după sine o redesenare a acesteia.
Nici unui joc nu ar trebui să îi lipsească componenta multiplayer.
Vom folosi Socket.io peste protocolul de transport WebSocket. Motivul este folosirea s-a mai simplă și faptul că ne scutește de câteva bătăi de cap legate de ID-ul unei conexiuni și modul de a trata mesajele primite.
const io = require('socket.io')(server,{
maxHttpBufferSize: 1e7 // 10 MB
});
SocketRoutes(io);
În continuare, vom implementa un protocol de comunicare între clienții care se vor conecta la serverul nostru astfel:
register
(acțiune) se creează o cameră de joc
(in) nume jucător
joinRoom
(acțiune) jucătorul este adăugat într-o cameră de joc
sendMove
(acțiune) se trimite poziția jucătorului și care este trimisă celuilalt participant
(in) numărul camerei, numele jucătorului, poziția paletei
io.on('connection', (socket) => {
function addPlayer(socket, index, name){
connections.push({
id: socket.id,
socket:socket,
index:index,
name: name,
});
return index;
}
function register(socket, name){
let index=connections.length;
addPlayer(socket, index, name);
return index;
}
socket.on('register', (data) => {
let index=register(socket, data.name);
socket.emit('registered', {
message: 'Welcome!',
index:index
});
});
socket.on('sendMove', (data) => {
connections.filter(el=>el.room===data.index)
.map(player=>{
if (player.name!==data.name) {
if (player.socket) {
player.socket.emit('sendMove', {
y:data.y,
name:data.name,
});
}
}
})
})
socket.on('joinRoom', (data) => {
addPlayer(socket, data.room, data.name);
})
});
Vom avea două tipuri de clienți: cei care inițiază un joc nou și cei care se alătură unuia existent. Cei din urmă vor putea scana un cod QR și să se alăture unuia existent.
socket = io(((SECURE)?'wss':'ws')+'://'+SERVER_NAME);
socket.on('connect', () => {
console.log('Connected with ID:', socket.id);
const params = new URLSearchParams(
window.location.search);
const indexParam=params.get('index');
if (indexParam){
roomIndex = indexParam;
socket.emit('joinRoom',{
room:indexParam,
name:"Player 2"
});
} else {
socket.emit('register', {
msg: "Hello from the browser!",
name:"Player 1"
});
}
});
Prin parametrul index, obținut prin scanarea codului QR putem diferenția ușor între cele două tipuri de jucători. Odată ce un jucător înregistrează o cameră nouă de joc, acesta poate să afișeze codul QR ce va codifica adresa serverului și parametrul index:
socket.on("registered", (data) => {
if (data.index !== undefined) {
roomIndex = data.index;
QRCode.toCanvas(
qrCanvas,
((SECURE ? "https" : "http") +
"://" + SERVER_NAME +"?index=" + data.index))
.then("canvas draw").catch(error => {
console.error("canvas error:", error);
});
qrText.style.display = "block";
startGame.style.display = "none";
}
});
Odată înregistrat, putem transmite mișcările paletelor:
socket.on('sendMove', (data) => {
if (data){
if ("Player 1"===data.name){
paddleLeft.setRemoteBallY(data.y)
}else {
paddleRight.setRemoteBallY(data.y)
}
}
});
Acestea se vor transmite doar dacă avem un socket activ:
function sendMove(y,name){
if (y!==undefined && socket && roomIndex){
//send move
socket.emit('sendMove',{
room:roomIndex,
y:y,
name:name,
})
}
}
function takePic() {
....
sendMove(paddleLeft.draw(), paddleLeft.name);
sendMove(paddleRight.draw(), paddleRight.name);
...
}
Următorii pași vor reprezenta finalizarea gameplay-ului după care putem continua cu o integrare IoT ceea ce ar adăuga un pic de magie. La final, ar fi interesant de realizat un jucător care se joacă și chiar învață din greșelile făcute prin AI.
de Ovidiu Mățan
de Ovidiu Mățan
de Radu Baciu
de Vlad Baesu