// 这里实现一个房间的等待逻辑,并且维护房间和玩家的状态。服务器只允许一个房间,房间最大8个玩家,默认情况房间是关闭的,如果有第一个玩家加入进来,则房间状态更新为开启,此后每有一个玩家加入房间,所有房间内的用户都会收到ws消息。每有一个玩家退出连接,所有房间内的用户也会收到ws消息。如果房间内已有8名在线玩家,则房间状态需要更新为锁定,并且给所有客户端发送游戏即将开始的ws消息。 // 你要实现一个接口,供前端点击“加入房间”按钮时调用,然后检查当前的房间状态。如果房间锁定了,需要返回消息给前端提示其等待。否则相应地更新房间的状态。 // waiting_room.js const express = require('express'); const pool = require('./db'); const { verifyToken } = require('./jwt'); const { broadcastMessage, sendMessageToUser, getClients } = require('./websocket'); // 只单向依赖 const router = express.Router(); const { startGame } = require('./game'); const { room: roominfo } = require('./room'); // 从 room.js 导入 // 房间状态 const room = roominfo; // 定时器 let statusCheckInterval; const clients = getClients(); /** * 从房间中移除指定 userId 的玩家并广播 * @param {string|number} userId */ function removePlayerFromRoom(userId) { const index = room.players.findIndex(player => player.userId === userId); if (index !== -1) { const [removedPlayer] = room.players.splice(index, 1); updateRoomStatus(); broadcastMessage('系统', `${removedPlayer.username} 离开了房间,当前玩家数: ${room.players.length}/${room.maxPlayers}`); broadcastPlayerStatus(); // 广播最新玩家列表 } } /** * 当 WebSocket 真的断线时,会通过回调方式调用此函数 * @param {string|number} userId */ function onUserDisconnected(userId) { removePlayerFromRoom(userId); } /** * 检查房间状态 */ function updateRoomStatus() { if (room.players.length === 0) { room.status = 'closed'; clearInterval(statusCheckInterval); // 如果房间没人,停止状态检查 statusCheckInterval = null; // 清空定时器变量 } else if (room.players.length < room.maxPlayers) { room.status = 'open'; startStatusCheck(); // 确保定时器重新启动 } else { room.status = 'locked'; broadcastMessage('系统', '房间已满,游戏将在5秒后开始!'); // 设置 5 秒后启动游戏 setTimeout(() => { startGame(room); }, 5000); // 5000 毫秒 = 5 秒 } } /** * 广播所有玩家状态 */ function broadcastPlayerStatus() { const playerStates = room.players.map(player => ({ userId: player.userId, username: player.username, status: player.status, })); broadcastMessage('系统', { type: 'player_status', players: playerStates, }); } /** * 清理状态异常的玩家 */ function cleanErrorPlayers() { const initialPlayerCount = room.players.length; room.players = room.players.filter(player => player.status !== 'error'); const cleanedPlayerCount = room.players.length; if (cleanedPlayerCount < initialPlayerCount) { console.log(`清理了 ${initialPlayerCount - cleanedPlayerCount} 名异常玩家`); updateRoomStatus(); // 更新房间状态 broadcastPlayerStatus(); // 广播状态更新 } } /** * 定时检查 WebSocket 连接稳定性 */ function startStatusCheck() { if (statusCheckInterval) { console.log('状态检查已在运行'); return; // 如果定时器已存在,直接返回 } console.log('启动状态检查定时器'); statusCheckInterval = setInterval(() => { room.players.forEach(player => { // 发送 ping 消息 try { if (player.ws.readyState === player.ws.OPEN) { player.ws.send( JSON.stringify({ type: 'ping', }) ); // 如果 pong 消息未及时收到,标记为异常 const timeout = setTimeout(() => { if (player.status !== 'active') { player.status = 'error'; broadcastPlayerStatus(); // 广播状态更新 } }, 5000); // 等待 5 秒 // 设置 pong 消息的监听 player.ws.once('message', (message) => { try { const data = JSON.parse(message); if (data.type === 'pong') { player.status = 'active'; // 接收到 pong,连接正常 clearTimeout(timeout); // 清除超时检测 } } catch (error) { console.error('解析消息失败:', error.message); } }); } } catch (error) { console.error(`检测 userId=${player.userId} 的连接时出错:`, error.message); player.status = 'error'; } }); // 清理异常玩家 cleanErrorPlayers(); // 广播所有玩家状态 broadcastPlayerStatus(); }, 10000); // 每 10 秒检测一次 } /** * 加入房间接口 */ router.post('/join', async (req, res) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ message: '未提供有效的 Authorization Header' }); } const token = authHeader.split(' ')[1]; try { const result = verifyToken(token); // 验证 Token // 检查 Token 是否包含错误 if (result.error) { return res.status(401).json({ message: result.error }); } const userId = result.userId; // 提取 userId const [users] = await pool.execute('SELECT id, username FROM users WHERE id = ?', [userId]); if (users.length === 0) { return res.status(404).json({ message: '用户不存在' }); } const { username } = users[0]; // 检查房间状态 if (room.status === 'locked') { return res.status(403).json({ message: '房间已满,请等待游戏结束后再加入' }); } // 从 WebSocket 服务中获取用户的 ws 对象 const ws = clients.get(userId); if (!ws) { return res.status(500).json({ message: 'WebSocket 未找到,无法加入房间' }); } // 检查用户是否已经在房间 const existingPlayer = room.players.find(player => player.userId === userId); if (existingPlayer) { // 如果用户已经在房间,更新其状态和 WebSocket existingPlayer.status = 'active'; existingPlayer.ws = ws; existingPlayer.username = username; // 更新用户名(如果可能更改) existingPlayer.lastPing = Date.now(); // 更新心跳时间 console.log(`用户 ${username} (ID: ${userId}) 状态已更新`); } else { // 用户不在房间时,添加新玩家 room.players.push({ userId, username, status: 'active', ws, lastPing: Date.now(), }); updateRoomStatus(); // 通知房间内的所有玩家 broadcastMessage('系统', `${username} 加入了房间,当前玩家数: ${room.players.length}/8`); // 立即广播所有玩家的状态 broadcastPlayerStatus(); // 开始状态检查 startStatusCheck(); } // 返回房间状态 return res.status(200).json({ message: '成功加入房间', roomStatus: room.status, players: room.players.map(player => ({ userId: player.userId, username: player.username, status: player.status, })), }); } catch (error) { console.error('加入房间时出错:', error.message); return res.status(500).json({ message: '服务器错误' }); } }); /** * 退出房间接口 */ router.post('/leave', async (req, res) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ message: '未提供有效的 Authorization Header' }); } const token = authHeader.split(' ')[1]; try { const result = verifyToken(token); if (result.error) { return res.status(401).json({ message: result.error }); } const userId = result.userId; // 从房间中移除玩家 removePlayerFromRoom(userId); return res.status(200).json({ message: '成功退出房间' }); } catch (error) { console.error('退出房间时出错:', error.message); return res.status(500).json({ message: '服务器错误' }); } }); module.exports = { router, onUserDisconnected, // 供外部(websocket.js 初始化时)设置断线回调 }; // 前端需确保 WebSocket 客户端正确响应 ping 消息: // ws.onmessage = (event) => { // const data = JSON.parse(event.data); // if (data.type === 'ping') { // ws.send( // JSON.stringify({ // type: 'pong', // }) // ); // } else if (data.type === 'player_status') { // console.log('房间内玩家状态:', data.players); // } // };