You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

页面刷新时如何避免新建Socket连接?游戏进度丢失问题求助

解决Socket游戏刷新页面丢失进度&重复开新游戏的思路

首先得明确核心问题:刷新页面本质是旧Socket连接断开+新Socket连接建立,但目前你的服务器没把这两个连接关联成同一个玩家,也没保留断开期间的游戏状态,才导致进度丢失、新对手误匹配。下面从onClientdisconnect()函数入手,一步步拆解解决方案:

1. 先给玩家一个「持久化身份」

刷新页面后,服务器得认出这是之前的玩家,而不是新用户。

  • 客户端第一次连接时,生成一个唯一ID(比如UUID),存在localStorage里;
  • 后续重连(包括刷新后),把这个ID通过Socket发送给服务器;
  • 服务器用这个ID作为玩家的唯一标识,而不是Socket的临时id。

2. 修改onClientdisconnect():不要立刻销毁游戏状态

断开连接时,先标记玩家为「离线等待重连」,而不是直接踢出去销毁游戏:

onClientdisconnect() {
  console.log('player disconnected with socket id ' + this.id);
  
  // 1. 找到该玩家所在的游戏房间(假设你用activeGames数组存所有活跃游戏)
  const targetGame = activeGames.find(game => 
    game.players.some(p => p.socketId === this.id)
  );
  if (!targetGame) return;

  // 2. 标记玩家为离线,记录断开时间
  const offlinePlayer = targetGame.players.find(p => p.socketId === this.id);
  offlinePlayer.isOffline = true;
  offlinePlayer.disconnectTime = Date.now();

  // 3. 给房间内其他玩家发通知:有人暂时断开
  this.broadcast.to(targetGame.roomId).emit('player-temp-disconnect', {
    playerName: offlinePlayer.name,
    message: '玩家暂时断开,等待重连中...'
  });

  // 4. 设置重连超时定时器(比如30秒,可根据游戏类型调整)
  targetGame.reconnectTimer = setTimeout(() => {
    // 超时未重连,才真正移除玩家
    targetGame.players = targetGame.players.filter(p => p.id !== offlinePlayer.id);
    
    // 处理剩余玩家:如果只剩1人/没人,销毁游戏;否则开放匹配新对手
    if (targetGame.players.length < 2) {
      // 清理游戏状态
      activeGames = activeGames.filter(g => g.roomId !== targetGame.roomId);
      this.broadcast.to(targetGame.roomId).emit('game-end', { reason: '玩家超时未重连' });
    } else {
      // 将空位置加入匹配池,等待新对手
      addToMatchQueue(targetGame.roomId);
    }
  }, 30000); // 30秒超时窗口
}

3. 新增「重连匹配」逻辑

客户端刷新后建立新连接时,带上之前的持久化玩家ID,服务器检查是否有未超时的游戏等待他:

// 客户端重连时发送身份标识
const savedPlayerId = localStorage.getItem('playerId');
if (savedPlayerId) {
  socket.emit('attempt-reconnect', { playerId: savedPlayerId });
}

// 服务器端监听重连请求
socket.on('attempt-reconnect', (data) => {
  const { playerId } = data;
  // 查找是否有该玩家的未超时离线游戏
  const pendingGame = activeGames.find(game => 
    game.players.some(p => p.id === playerId && p.isOffline)
  );

  if (pendingGame) {
    // 取消之前的超时销毁定时器
    clearTimeout(pendingGame.reconnectTimer);
    // 更新玩家的Socket ID为新连接ID,标记为在线
    const reconnectingPlayer = pendingGame.players.find(p => p.id === playerId);
    reconnectingPlayer.socketId = socket.id;
    reconnectingPlayer.isOffline = false;

    // 让新Socket加入原房间
    socket.join(pendingGame.roomId);
    // 把当前游戏状态同步给重连的玩家
    socket.emit('restore-game-state', pendingGame.currentState);
    // 通知房间内其他玩家:此人已重连
    socket.broadcast.to(pendingGame.roomId).emit('player-reconnected', { playerId });
  } else {
    // 没有待重连的游戏,进入正常匹配流程
    startNewMatching(socket);
  }
});

4. 游戏状态要「脱离Socket实例存储」

把游戏的核心状态(比如回合数、得分、玩家位置等)存在独立的对象/内存数据库(如Redis)里,而不是Socket连接的属性中——因为Socket断开后,实例会被销毁,状态也跟着没了。

比如用一个全局对象管理所有活跃游戏:

// 服务器端全局变量
const activeGames = [
  {
    roomId: 'room-123',
    players: [
      { id: 'player-uuid-1', socketId: 'socket-xxx', isOffline: false, name: '玩家A' },
      { id: 'player-uuid-2', socketId: 'socket-yyy', isOffline: false, name: '玩家B' }
    ],
    currentState: { round: 3, scores: { 'player-uuid-1': 10, 'player-uuid-2': 8 } },
    reconnectTimer: null
  }
];

额外优化点

  • 如果是无需登录的游戏,客户端第一次连接时自动生成UUID并存在localStorage,避免用户手动输入;
  • 对于实时性要求高的游戏,可以在玩家离线期间,把关键操作暂存,重连后同步给玩家;
  • 若服务器需要高可用性,用Redis替代内存存储游戏状态,防止服务器重启丢失所有进度。

内容的提问来源于stack exchange,提问作者roh_dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 03:51:49