293 lines
8.8 KiB
JavaScript
293 lines
8.8 KiB
JavaScript
|
||
|
||
// 这里实现一个房间的等待逻辑,并且维护房间和玩家的状态。服务器只允许一个房间,房间最大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);
|
||
// }
|
||
// };
|