WebSockets والتطبيقات الفورية

بناء مشروع لعبة متعددة اللاعبين

20 دقيقة الدرس 34 من 35

بناء مشروع لعبة متعددة اللاعبين

في هذا الدرس، سنبني لعبة متعددة اللاعبين في الوقت الفعلي باستخدام WebSockets. سننشئ لعبة بسيطة ولكن كاملة توضح مزامنة اللاعبين، وإدارة الغرف، وتحديثات حالة اللعبة في الوقت الفعلي.

بنية اللعبة

ستتضمن لعبتنا متعددة اللاعبين:

ميزات اللعبة:
  • مصادقة اللاعب ومعرفات فريدة
  • إنشاء غرف اللعبة والانضمام إليها
  • مزامنة حركة اللاعب في الوقت الفعلي
  • كشف التصادم وتتبع النقاط
  • إدارة حالة اللعبة (انتظار، لعب، انتهت اللعبة)
  • وضع المشاهد للمراقبين

إعداد الخادم مع منطق اللعبة

لنقم بإنشاء خادم اللعبة باستخدام Socket.io:

const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: '*', methods: ['GET', 'POST'] } }); // إدارة حالة اللعبة const games = new Map(); const players = new Map(); class Game { constructor(roomId, maxPlayers = 4) { this.roomId = roomId; this.maxPlayers = maxPlayers; this.players = new Map(); this.state = 'waiting'; // waiting, playing, finished this.startTime = null; this.gameLoop = null; this.collectibles = []; this.scores = new Map(); } addPlayer(playerId, playerData) { if (this.players.size >= this.maxPlayers) { return { success: false, error: 'الغرفة ممتلئة' }; } this.players.set(playerId, { id: playerId, name: playerData.name, x: Math.random() * 800, y: Math.random() * 600, color: this.getRandomColor(), score: 0, isReady: false }); this.scores.set(playerId, 0); return { success: true }; } removePlayer(playerId) { this.players.delete(playerId); this.scores.delete(playerId); if (this.players.size === 0) { this.stop(); } } getRandomColor() { const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8']; return colors[Math.floor(Math.random() * colors.length)]; } setPlayerReady(playerId) { const player = this.players.get(playerId); if (player) { player.isReady = true; // بدء اللعبة إذا كان جميع اللاعبين جاهزين const allReady = Array.from(this.players.values()).every(p => p.isReady); if (allReady && this.players.size >= 2) { this.start(); } } } start() { if (this.state === 'playing') return; this.state = 'playing'; this.startTime = Date.now(); this.spawnCollectibles(); // حلقة اللعبة - تحديث كل 16ms (~60 FPS) this.gameLoop = setInterval(() => { this.update(); }, 16); } spawnCollectibles() { this.collectibles = []; for (let i = 0; i < 10; i++) { this.collectibles.push({ id: i, x: Math.random() * 800, y: Math.random() * 600, type: Math.random() > 0.7 ? 'bonus' : 'normal', value: Math.random() > 0.7 ? 10 : 5 }); } } update() { // التحقق من تصادمات العناصر القابلة للجمع this.players.forEach((player, playerId) => { this.collectibles = this.collectibles.filter(collectible => { const distance = Math.sqrt( Math.pow(player.x - collectible.x, 2) + Math.pow(player.y - collectible.y, 2) ); if (distance < 30) { // اللاعب جمع العنصر const currentScore = this.scores.get(playerId) || 0; this.scores.set(playerId, currentScore + collectible.value); player.score = this.scores.get(playerId); return false; // إزالة العنصر القابل للجمع } return true; }); }); // إنشاء عناصر قابلة للجمع جديدة إذا لزم الأمر if (this.collectibles.length < 5) { this.collectibles.push({ id: Date.now(), x: Math.random() * 800, y: Math.random() * 600, type: Math.random() > 0.7 ? 'bonus' : 'normal', value: Math.random() > 0.7 ? 10 : 5 }); } // التحقق من شرط انتهاء اللعبة (دقيقتان) const elapsed = Date.now() - this.startTime; if (elapsed > 120000) { this.finish(); } } updatePlayerPosition(playerId, x, y) { const player = this.players.get(playerId); if (player) { player.x = Math.max(0, Math.min(800, x)); player.y = Math.max(0, Math.min(600, y)); } } finish() { this.state = 'finished'; if (this.gameLoop) { clearInterval(this.gameLoop); this.gameLoop = null; } // تحديد الفائز let winner = null; let maxScore = 0; this.scores.forEach((score, playerId) => { if (score > maxScore) { maxScore = score; winner = this.players.get(playerId); } }); return { winner: winner ? winner.name : 'لا فائز', scores: Array.from(this.scores.entries()).map(([id, score]) => ({ player: this.players.get(id).name, score })) }; } stop() { if (this.gameLoop) { clearInterval(this.gameLoop); } games.delete(this.roomId); } getState() { return { roomId: this.roomId, state: this.state, players: Array.from(this.players.values()), collectibles: this.collectibles, scores: Array.from(this.scores.entries()).map(([id, score]) => ({ playerId: id, playerName: this.players.get(id)?.name, score })) }; } } // معالجة اتصال Socket.io io.on('connection', (socket) => { console.log('لاعب متصل:', socket.id); // إنشاء غرفة لعبة أو الانضمام إليها socket.on('game:join', (data) => { const { roomId, playerName } = data; let game = games.get(roomId); if (!game) { game = new Game(roomId); games.set(roomId, game); } const result = game.addPlayer(socket.id, { name: playerName }); if (result.success) { socket.join(roomId); players.set(socket.id, roomId); socket.emit('game:joined', { success: true, playerId: socket.id, gameState: game.getState() }); // إخطار جميع اللاعبين في الغرفة io.to(roomId).emit('game:playerJoined', { playerId: socket.id, playerName, gameState: game.getState() }); } else { socket.emit('game:joined', { success: false, error: result.error }); } }); // اللاعب جاهز socket.on('game:ready', () => { const roomId = players.get(socket.id); const game = games.get(roomId); if (game) { game.setPlayerReady(socket.id); io.to(roomId).emit('game:stateUpdate', game.getState()); if (game.state === 'playing') { io.to(roomId).emit('game:started'); } } }); // حركة اللاعب socket.on('game:move', (data) => { const roomId = players.get(socket.id); const game = games.get(roomId); if (game && game.state === 'playing') { game.updatePlayerPosition(socket.id, data.x, data.y); // البث إلى اللاعبين الآخرين socket.to(roomId).emit('game:playerMoved', { playerId: socket.id, x: data.x, y: data.y }); } }); // تحديثات حالة اللعبة setInterval(() => { const roomId = players.get(socket.id); const game = games.get(roomId); if (game && game.state === 'playing') { io.to(roomId).emit('game:stateUpdate', game.getState()); } }, 100); // التحديث 10 مرات في الثانية // قطع الاتصال socket.on('disconnect', () => { const roomId = players.get(socket.id); const game = games.get(roomId); if (game) { game.removePlayer(socket.id); io.to(roomId).emit('game:playerLeft', { playerId: socket.id, gameState: game.getState() }); } players.delete(socket.id); console.log('لاعب غير متصل:', socket.id); }); }); server.listen(3000, () => { console.log('خادم اللعبة يعمل على المنفذ 3000'); });
نصيحة: بالنسبة لألعاب الإنتاج، نفذ التنبؤ من جانب العميل والمطابقة من جانب الخادم للتعامل مع زمن انتقال الشبكة بسلاسة. هذا يمنع اللعبة من الشعور بالبطء.

تقنيات تحسين اللعبة

قم بتحسين لعبتك متعددة اللاعبين لتحسين الأداء:

// جانب الخادم: تقليل تكرار التحديث للبيانات غير الحرجة const updateIntervals = { positions: 50, // 20 تحديث/ثانية scores: 100, // 10 تحديثات/ثانية gameState: 200 // 5 تحديثات/ثانية }; // جانب العميل: الاستيفاء لحركة سلسة class PlayerInterpolator { constructor() { this.positions = []; } addPosition(x, y, timestamp) { this.positions.push({ x, y, timestamp }); if (this.positions.length > 10) { this.positions.shift(); } } interpolate(currentTime) { if (this.positions.length < 2) return this.positions[0]; const renderDelay = 100; // 100ms خلف الخادم const targetTime = currentTime - renderDelay; // العثور على موقعين للاستيفاء بينهما let before = this.positions[0]; let after = this.positions[1]; for (let i = 0; i < this.positions.length - 1; i++) { if (this.positions[i].timestamp <= targetTime && this.positions[i + 1].timestamp >= targetTime) { before = this.positions[i]; after = this.positions[i + 1]; break; } } const t = (targetTime - before.timestamp) / (after.timestamp - before.timestamp); return { x: before.x + (after.x - before.x) * t, y: before.y + (after.y - before.y) * t }; } }
تحذير: تحقق دائماً من إجراءات اللاعب على الخادم. لا تثق أبداً بتحديثات الموقع من جانب العميل دون التحقق من جانب الخادم لمنع الغش.
تمرين: قم بتوسيع اللعبة متعددة اللاعبين بميزات إضافية: (1) أضف power-ups التي تمنح دفعة سرعة مؤقتة، (2) نفذ نظام ردهة حيث يمكن للاعبين رؤية الغرف المتاحة، (3) أضف وضع المشاهد للاعبين الذين ينضمون إلى غرف ممتلئة، (4) أنشئ نظام إعادة تشغيل يسجل ويعيد تشغيل المباريات، (5) نفذ التحقق من مكافحة الغش لمنع اللاعبين من النقل الفوري أو التحرك بسرعة كبيرة جداً.