2026年03月30日25 分钟

微信小游戏后端开发(三):房间系统(双人联机核心)

从 0 实现双人房间系统,支持创建房间、加入房间、房间内广播、断线清理与空房销毁,包含完整代码与实测流程。

微信小游戏后端开发(三):房间系统(双人联机核心)

上一篇我们完成了 第 2 步:WebSocket 战斗服务,已经具备:

  • 客户端连接
  • 广播消息
  • 心跳检测 ping/pong
  • PM2 托管
  • Nginx 代理 /ws/

但那一版仍然是“全局广播”:

任意一个客户端发消息,所有在线客户端都能收到。

这对于调试来说没问题,但对于真正的双人联机坦克游戏来说还远远不够。
因为真实游戏应该是:

  • A 和 B 在房间 1001
  • C 和 D 在房间 1002
  • 1001 的消息不能发给 1002
  • 每个房间最多 2 人

所以这一篇我们正式进入:

第 3 步:房间系统


🧠 1. 本篇目标

这一篇要解决的问题很明确:

这一阶段要做什么?

我们要把当前的 WebSocket 服务,从“全局广播版”升级成“房间版”,实现:

  • 创建房间
  • 加入房间
  • 房间人数限制为 2
  • 房间内广播
  • 断线自动离开房间
  • 空房间自动销毁
  • 查询当前房间列表(调试用)

最终达到的效果是:

两个客户端可以在同一个房间内通信,第三个客户端不能挤进去,不同房间互不干扰。


在整个架构中的位置

整体路线保持不变:

  • 第 1 步:基础服务器搭建(已完成)
  • 第 2 步:WebSocket 战斗服务(已完成)
  • 第 3 步:房间系统(当前阶段)
  • 第 4 步:战斗同步
  • 第 5 步:数据系统(MySQL + Redis)
  • 第 6 步:微信接入 + 架构优化

你可以把第 3 步理解成:

给双人联机加上“谁跟谁打”的规则层。


🧱 2. 架构图(必须先看懂)

这一阶段的整体入口没有变化,但 WebSocket 服务内部已经开始分层:

graph TD
客户端 --> Nginx
Nginx --> API服务
Nginx --> WebSocket服务
WebSocket服务 --> 房间系统

如果再展开一点,可以理解成这样:

graph TD
A[客户端A] --> N[Nginx]
B[客户端B] --> N
C[客户端C] --> N
N --> API[Express API 服务 :7001]
N --> WS[WebSocket 战斗服务 :7002]
WS --> ROOM[内存房间表 rooms]

这张图怎么理解?

  • 客户端:微信小游戏、浏览器测试页、或者 wscat
  • Nginx:统一公网入口
  • API 服务:处理 HTTP 接口
  • WebSocket 服务:处理实时长连接
  • 房间系统:在 WebSocket 服务内部,负责把不同玩家分组

为什么房间系统先做成“内存版”?

因为当前目标是:

先把房间逻辑跑通

而不是一开始就把 Redis、数据库、匹配服务全上齐。
当前阶段用内存版最简单,也最适合新手理解。


🚀 3. 一步一步实操(重点)

这一部分按真实开发顺序来,不跳步骤。


第一步:先理解房间最小模型

在开始写代码前,先把房间在服务端怎么存想清楚。

我们需要两个状态:

1)全局房间表

const rooms = new Map();

它表示:

  • key:roomId
  • value:房间对象

例如:

{
  roomId: "1001",
  players: [ws1, ws2]
}

2)每个连接当前在哪个房间

ws.roomId = null;

如果玩家已经加入房间:

ws.roomId = "1001";

为什么这么设计?

因为后面服务端处理消息时,必须知道两件事:

  1. 当前所有房间有哪些
  2. 当前这个连接属于哪个房间

只有这样,才能做到“只发给房间内的人”。


第二步:先备份第 2 步的 server.js

进入 battle 目录:

cd /srv/tank-game/battle
cp server.js server.step2.backup.js

为什么一定先备份?

因为第 2 步已经是一个能跑的版本了。
先备份的好处是:

  • 万一新代码出错,可以快速回滚
  • 不会把已经能跑的版本覆盖丢失
  • 这是正式开发里很好的习惯

第三步:用房间系统版代码替换 server.js

执行:

nano /srv/tank-game/battle/server.js

然后把下面完整代码全部替换进去。

完整代码在下面“完整代码”章节给出。

为什么这一步直接整份替换?

因为房间系统涉及的内容比较多:

  • 全局房间表
  • 单个连接状态
  • 消息协议解析
  • 房间广播
  • 断线清理
  • 空房销毁

如果只给零散代码片段,新手很容易漏掉上下文。
所以当前阶段最稳的方式就是:

直接整份替换 server.js


第四步:重启 battle 服务

代码替换完成后,执行:

pm2 restart tank-battle

然后查看日志:

pm2 logs tank-battle --lines 30

为什么这里不用改 Nginx?

因为这一步改的是 battle 服务内部逻辑,没有改:

  • 端口
  • Nginx 路径
  • PM2 进程名

所以外层部署方式不变,只要让 PM2 重新加载代码即可。


第五步:连接 3 个客户端进行联调

建议使用 wscat:

wscat -c ws://127.0.0.1/ws/

开 3 个终端窗口,分别连接 3 个客户端。

为什么建议开 3 个?

因为这样一次性能测出:

  • 客户端 1:创建房间
  • 客户端 2:加入房间
  • 客户端 3:尝试加入同一个房间,被拒绝

这正好把“双人限制”也一起验掉。


第六步:按顺序发送测试消息

客户端 1:先看房间列表

{"type":"get_rooms"}

预期返回:

{"type":"room_list","rooms":[],"timestamp":...}

客户端 1:创建房间

{"type":"create_room","roomId":"1001"}

客户端 2:加入房间

{"type":"join_room","roomId":"1001"}

客户端 3:尝试加入同一个房间

{"type":"join_room","roomId":"1001"}

预期返回错误:

{
  "type":"error",
  "message":"加入房间失败:房间 1001 已满(最多 2 人)"
}

客户端 1:发送房间消息

{"type":"room_chat","message":"hello room 1001 from client1"}

预期结果:

  • 客户端 1 收到
  • 客户端 2 收到
  • 客户端 3 收不到

客户端 3:在未加入房间的情况下发房间消息

{"type":"room_chat","message":"i am not in room"}

预期返回错误:

{
  "type":"error",
  "message":"发送房间消息失败:你还没有加入任何房间"
}

第七步:测试断线清理

先关闭客户端 2:

Ctrl + C

然后在客户端 1 里执行:

{"type":"get_rooms"}

此时预期应该只剩下 1 人。

再关闭客户端 1:

Ctrl + C

服务端应自动销毁空房。

最后新开一个客户端查询:

{"type":"get_rooms"}

预期返回:

{"type":"room_list","rooms":[],"timestamp":...}

为什么要专门测断线清理?

因为真实游戏里玩家掉线是高频事件。
如果断线不清理,就会出现:

  • 房间里残留脏连接
  • 房间明明空了却不销毁
  • 后面玩家再进房间时状态错乱

所以断线清理是房间系统非常关键的一环。


💻 4. 完整代码(必须完整)

下面是房间系统版的完整 server.js。
这份代码包含:

  • 连接
  • 房间创建
  • 房间加入
  • 房间广播
  • 房间列表查询
  • ping/pong
  • 断线离房
  • 空房销毁
const http = require('http');
const WebSocket = require('ws');

// ================================
// 1. 基础配置
// ================================
const HOST = '0.0.0.0';
const PORT = 7002;

// 自增客户端ID
let nextClientId = 1;

// 房间表
// key: roomId
// value: { roomId, players: [ws1, ws2] }
const rooms = new Map();

// ================================
// 2. 创建 HTTP 服务
// ================================
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  res.end('Tank Battle Room Server is running.\n');
});

// ================================
// 3. 创建 WebSocket 服务
// ================================
const wss = new WebSocket.Server({ server });

// ================================
// 4. 工具函数
// ================================

// 发消息给单个客户端
function sendToClient(ws, data) {
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(data));
  }
}

// 获取房间对象
function getRoom(roomId) {
  return rooms.get(roomId);
}

// 广播给指定房间内所有玩家
function broadcastToRoom(roomId, data) {
  const room = rooms.get(roomId);
  if (!room) return;

  const message = JSON.stringify(data);

  room.players.forEach((player) => {
    if (player.readyState === WebSocket.OPEN) {
      player.send(message);
    }
  });
}

// 广播给房间内除自己以外的玩家
function broadcastToRoomExceptSelf(roomId, sender, data) {
  const room = rooms.get(roomId);
  if (!room) return;

  const message = JSON.stringify(data);

  room.players.forEach((player) => {
    if (player !== sender && player.readyState === WebSocket.OPEN) {
      player.send(message);
    }
  });
}

// 返回当前所有房间的简化信息(便于调试)
function getRoomList() {
  const list = [];

  rooms.forEach((room, roomId) => {
    list.push({
      roomId,
      playerCount: room.players.length,
      playerIds: room.players.map((player) => player.clientId)
    });
  });

  return list;
}

// 玩家离开当前房间
function leaveCurrentRoom(ws) {
  if (!ws.roomId) {
    return;
  }

  const roomId = ws.roomId;
  const room = rooms.get(roomId);

  if (!room) {
    ws.roomId = null;
    return;
  }

  room.players = room.players.filter((player) => player !== ws);

  console.log(`[房间离开] clientId=${ws.clientId} 离开房间 roomId=${roomId}`);

  // 通知房间内剩余玩家
  broadcastToRoom(roomId, {
    type: 'room_system',
    roomId,
    message: `客户端 ${ws.clientId} 已离开房间`,
    playerCount: room.players.length,
    timestamp: Date.now()
  });

  // 如果房间空了,就销毁房间
  if (room.players.length === 0) {
    rooms.delete(roomId);
    console.log(`[房间销毁] roomId=${roomId} 已无玩家,自动销毁`);
  } else {
    rooms.set(roomId, room);
  }

  ws.roomId = null;
}

// 创建房间
function handleCreateRoom(ws, data) {
  const roomId = String(data.roomId || '').trim();

  if (!roomId) {
    return sendToClient(ws, {
      type: 'error',
      message: '创建房间失败:roomId 不能为空',
      timestamp: Date.now()
    });
  }

  if (rooms.has(roomId)) {
    return sendToClient(ws, {
      type: 'error',
      message: `创建房间失败:房间 ${roomId} 已存在`,
      timestamp: Date.now()
    });
  }

  // 如果当前已经在别的房间里,先离开旧房间
  if (ws.roomId) {
    leaveCurrentRoom(ws);
  }

  const room = {
    roomId,
    players: [ws]
  };

  rooms.set(roomId, room);
  ws.roomId = roomId;

  console.log(`[房间创建] clientId=${ws.clientId} 创建房间 roomId=${roomId}`);

  sendToClient(ws, {
    type: 'create_room_success',
    roomId,
    clientId: ws.clientId,
    playerCount: room.players.length,
    timestamp: Date.now()
  });

  broadcastToRoom(roomId, {
    type: 'room_system',
    roomId,
    message: `客户端 ${ws.clientId} 创建并加入了房间`,
    playerCount: room.players.length,
    timestamp: Date.now()
  });
}

// 加入房间
function handleJoinRoom(ws, data) {
  const roomId = String(data.roomId || '').trim();

  if (!roomId) {
    return sendToClient(ws, {
      type: 'error',
      message: '加入房间失败:roomId 不能为空',
      timestamp: Date.now()
    });
  }

  const room = rooms.get(roomId);

  if (!room) {
    return sendToClient(ws, {
      type: 'error',
      message: `加入房间失败:房间 ${roomId} 不存在`,
      timestamp: Date.now()
    });
  }

  if (room.players.includes(ws)) {
    return sendToClient(ws, {
      type: 'error',
      message: `加入房间失败:你已经在房间 ${roomId} 中`,
      timestamp: Date.now()
    });
  }

  if (room.players.length >= 2) {
    return sendToClient(ws, {
      type: 'error',
      message: `加入房间失败:房间 ${roomId} 已满(最多 2 人)`,
      timestamp: Date.now()
    });
  }

  // 如果当前已经在别的房间里,先离开旧房间
  if (ws.roomId && ws.roomId !== roomId) {
    leaveCurrentRoom(ws);
  }

  room.players.push(ws);
  ws.roomId = roomId;

  console.log(`[房间加入] clientId=${ws.clientId} 加入房间 roomId=${roomId}`);

  sendToClient(ws, {
    type: 'join_room_success',
    roomId,
    clientId: ws.clientId,
    playerCount: room.players.length,
    timestamp: Date.now()
  });

  broadcastToRoom(roomId, {
    type: 'room_system',
    roomId,
    message: `客户端 ${ws.clientId} 加入了房间`,
    playerCount: room.players.length,
    timestamp: Date.now()
  });
}

// 房间内聊天/广播
function handleRoomChat(ws, data) {
  if (!ws.roomId) {
    return sendToClient(ws, {
      type: 'error',
      message: '发送房间消息失败:你还没有加入任何房间',
      timestamp: Date.now()
    });
  }

  const message = String(data.message || '').trim();

  if (!message) {
    return sendToClient(ws, {
      type: 'error',
      message: '发送房间消息失败:message 不能为空',
      timestamp: Date.now()
    });
  }

  const room = rooms.get(ws.roomId);

  if (!room) {
    return sendToClient(ws, {
      type: 'error',
      message: '发送房间消息失败:当前房间不存在',
      timestamp: Date.now()
    });
  }

  console.log(
    `[房间消息] roomId=${ws.roomId} clientId=${ws.clientId} message=${message}`
  );

  broadcastToRoom(ws.roomId, {
    type: 'room_chat',
    roomId: ws.roomId,
    clientId: ws.clientId,
    message,
    playerCount: room.players.length,
    timestamp: Date.now()
  });
}

// 查看房间列表(调试用)
function handleGetRooms(ws) {
  sendToClient(ws, {
    type: 'room_list',
    rooms: getRoomList(),
    timestamp: Date.now()
  });
}

// ================================
// 5. 连接事件
// ================================
wss.on('connection', (ws, req) => {
  const clientId = nextClientId++;
  const clientIp =
    req.headers['x-forwarded-for'] ||
    req.socket.remoteAddress ||
    'unknown';

  ws.clientId = clientId;
  ws.roomId = null;
  ws.isAlive = true;

  console.log('-----------------------------------');
  console.log('[连接成功] 新客户端已连接');
  console.log('[客户端ID]', ws.clientId);
  console.log('[客户端IP]', clientIp);
  console.log('[当前在线数]', wss.clients.size);
  console.log('-----------------------------------');

  sendToClient(ws, {
    type: 'welcome',
    clientId: ws.clientId,
    message: '连接 battle WebSocket 服务成功',
    onlineCount: wss.clients.size,
    timestamp: Date.now()
  });

  // 收到消息
  ws.on('message', (message) => {
    try {
      const text = message.toString();
      console.log('[收到原始消息]', `clientId=${ws.clientId}`, text);

      let data;
      try {
        data = JSON.parse(text);
      } catch (parseError) {
        return sendToClient(ws, {
          type: 'error',
          message: '消息格式错误:请发送 JSON 字符串',
          example: JSON.stringify({
            type: 'create_room',
            roomId: '1001'
          }),
          timestamp: Date.now()
        });
      }

      const { type } = data;

      switch (type) {
        case 'create_room':
          handleCreateRoom(ws, data);
          break;

        case 'join_room':
          handleJoinRoom(ws, data);
          break;

        case 'room_chat':
          handleRoomChat(ws, data);
          break;

        case 'get_rooms':
          handleGetRooms(ws);
          break;

        default:
          sendToClient(ws, {
            type: 'error',
            message: `未知消息类型:${type}`,
            supportedTypes: ['create_room', 'join_room', 'room_chat', 'get_rooms'],
            timestamp: Date.now()
          });
          break;
      }
    } catch (error) {
      console.error('[消息处理失败]', `clientId=${ws.clientId}`, error);

      sendToClient(ws, {
        type: 'error',
        message: '服务器处理消息失败',
        timestamp: Date.now()
      });
    }
  });

  // 收到 pong
  ws.on('pong', () => {
    ws.isAlive = true;
    console.log('[心跳响应] 收到客户端 pong', `clientId=${ws.clientId}`);
  });

  // 连接关闭
  ws.on('close', () => {
    console.log('-----------------------------------');
    console.log('[连接关闭] 一个客户端已断开');
    console.log('[客户端ID]', ws.clientId);
    console.log('[当前在线数]', wss.clients.size);
    console.log('-----------------------------------');

    leaveCurrentRoom(ws);
  });

  // 连接异常
  ws.on('error', (error) => {
    console.error('[WebSocket 连接异常]', `clientId=${ws.clientId}`, error);
  });
});

// ================================
// 6. 心跳检测
// ================================
const heartbeatInterval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) {
      console.log('[心跳检测] 客户端无响应,主动断开', `clientId=${ws.clientId}`);
      return ws.terminate();
    }

    ws.isAlive = false;
    ws.ping();

    console.log('[心跳检测] 已发送 ping', `clientId=${ws.clientId}`);
  });
}, 30000);

wss.on('close', () => {
  clearInterval(heartbeatInterval);
});

// ================================
// 7. 启动服务
// ================================
server.listen(PORT, HOST, () => {
  console.log('===================================');
  console.log('Tank Battle Room Server 已启动');
  console.log(`监听地址: ws://${HOST}:${PORT}`);
  console.log('推荐代理入口: /ws/');
  console.log('已支持消息类型: create_room / join_room / room_chat / get_rooms');
  console.log('===================================');
});

🧪 5. 测试方法(非常关键)

代码写完后,测试一定不能省略。


方法一:使用 wscat 测试(推荐)

先安装:

npm install -g wscat

或者直接用:

npx wscat -c ws://127.0.0.1/ws/

1)连接 3 个客户端

开 3 个窗口,分别执行:

wscat -c ws://127.0.0.1/ws/

连接成功后,每个客户端都会收到欢迎消息,例如:

{
  "type":"welcome",
  "clientId":1,
  "message":"连接 battle WebSocket 服务成功",
  "onlineCount":1,
  "timestamp":...
}

2)查询房间列表

在客户端 1 发送:

{"type":"get_rooms"}

预期返回:

{"type":"room_list","rooms":[],"timestamp":...}

3)创建房间

客户端 1 发送:

{"type":"create_room","roomId":"1001"}

预期返回:

{
  "type":"create_room_success",
  "roomId":"1001",
  "clientId":1,
  "playerCount":1,
  "timestamp":...
}

4)加入房间

客户端 2 发送:

{"type":"join_room","roomId":"1001"}

预期返回:

{
  "type":"join_room_success",
  "roomId":"1001",
  "clientId":2,
  "playerCount":2,
  "timestamp":...
}

5)第三人加入失败

客户端 3 发送:

{"type":"join_room","roomId":"1001"}

预期返回:

{
  "type":"error",
  "message":"加入房间失败:房间 1001 已满(最多 2 人)",
  "timestamp":...
}

6)房间内聊天

客户端 1 发送:

{"type":"room_chat","message":"hello room 1001 from client1"}

预期结果:

  • 客户端 1 收到
  • 客户端 2 收到
  • 客户端 3 收不到

7)未加入房间不能发房间消息

客户端 3 发送:

{"type":"room_chat","message":"i am not in room"}

预期返回:

{
  "type":"error",
  "message":"发送房间消息失败:你还没有加入任何房间",
  "timestamp":...
}

8)测试断线清理

关闭客户端 2:

Ctrl + C

客户端 1 再执行:

{"type":"get_rooms"}

应看到房间只剩 1 人。

再关闭客户端 1:

Ctrl + C

此时服务端应打印:

[房间销毁] roomId=1001 已无玩家,自动销毁

最后新开一个客户端查询房间:

{"type":"get_rooms"}

应返回:

{"type":"room_list","rooms":[],"timestamp":...}

方法二:浏览器控制台测试

打开浏览器控制台,执行:

const ws = new WebSocket('ws://你的服务器公网IP/ws/');

ws.onopen = () => {
  console.log('WebSocket 已连接');
  ws.send(JSON.stringify({ type: 'get_rooms' }));
};

ws.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

ws.onclose = () => {
  console.log('连接已关闭');
};

ws.onerror = (err) => {
  console.error('连接出错:', err);
};

创建房间:

ws.send(JSON.stringify({ type: 'create_room', roomId: '1001' }));

发送房间消息:

ws.send(JSON.stringify({ type: 'room_chat', message: 'hello room from browser' }));

方法三:最小 HTML 测试页

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Room WebSocket Test</title>
</head>
<body>
  <h1>房间系统测试页</h1>

  <input id="msgInput" type="text" placeholder='输入 JSON,例如 {"type":"get_rooms"}' style="width: 520px;" />
  <button id="sendBtn">发送</button>

  <pre id="log"></pre>

  <script>
    const logEl = document.getElementById('log');
    const inputEl = document.getElementById('msgInput');
    const sendBtn = document.getElementById('sendBtn');

    const ws = new WebSocket('ws://你的服务器公网IP/ws/');

    function log(message) {
      logEl.textContent += message + '\n';
    }

    ws.onopen = () => {
      log('✅ WebSocket 已连接');
    };

    ws.onmessage = (event) => {
      log('📩 收到消息: ' + event.data);
    };

    ws.onclose = () => {
      log('❌ 连接已关闭');
    };

    ws.onerror = (error) => {
      log('⚠️ 连接异常: ' + JSON.stringify(error));
    };

    sendBtn.onclick = () => {
      const text = inputEl.value.trim();
      if (!text) return;

      ws.send(text);
      log('➡️ 已发送: ' + text);
      inputEl.value = '';
    };
  </script>
</body>
</html>

⚠️ 6. 踩坑记录(必须)

真实开发中,房间系统最容易踩的坑并不是“写不出代码”,而是状态管理没想清楚。


坑 1:发普通文本导致解析失败

现象

你像第 2 步那样直接发:

hello

结果收到错误。

原因

因为这一版服务已经要求客户端发送 JSON 字符串,服务端会先执行:

JSON.parse(text)

解决方法

必须按 JSON 发:

{"type":"create_room","roomId":"1001"}

坑 2:房间人数没有限制

现象

第三个客户端居然也能进来。

原因

加入房间时如果忘了判断人数,就会失去“双人房”的业务约束。

解决方法

必须加上:

if (room.players.length >= 2) {
  return sendToClient(ws, {
    type: 'error',
    message: `加入房间失败:房间 ${roomId} 已满(最多 2 人)`,
    timestamp: Date.now()
  });
}

坑 3:断线后不清理房间

现象

玩家明明已经掉线,但房间里还残留着它。

原因

只监听了 close,但没有在 close 时真正把玩家从房间数组里删掉。

解决方法

在 ws.on('close') 中调用:

leaveCurrentRoom(ws);

并在 leaveCurrentRoom 中完成:

  • 从房间里删除该玩家
  • 通知房间剩余玩家
  • 如果房间空了就销毁

坑 4:空房间不销毁

现象

玩家都退出了,但 get_rooms 还能看到空房间残留。

原因

只删除了房间里的玩家,没有处理“房间没人”的情况。

解决方法

必须加这段:

if (room.players.length === 0) {
  rooms.delete(roomId);
  console.log(`[房间销毁] roomId=${roomId} 已无玩家,自动销毁`);
}

坑 5:房间消息又发成了全局广播

现象

本来想只给房间成员发,结果所有客户端都收到了。

原因

不小心还在用全局广播函数。

解决方法

房间消息要走:

broadcastToRoom(ws.roomId, data);

而不是:

broadcast(data);

坑 6:客户端 3 看起来像加入成功,其实是终端输出贴错了

现象

调试时日志明明显示有 clientId=3,但终端输出贴出来却像客户端 2 的复制版。

原因

多窗口测试时,最容易贴错窗口内容。

解决方法

调试联机功能时建议:

  • 每个窗口都写清楚“客户端 1 / 2 / 3”
  • 重点观察 clientId
  • 同时结合服务端日志一起判断

📦 7. 本阶段成果总结

到这里,第 3 步已经真正完成闭环。

你已经具备了这些能力:

1. 双人房间系统

服务端已经能把连接从“全局池”分组到“房间”中,这是联机游戏后端第一次真正进入业务逻辑层。


2. 房间人数限制

每个房间最多 2 人,这完全贴合双人坦克游戏当前阶段的业务目标。


3. 房间内隔离广播

同一个房间里的玩家可以互相通信,不同房间不会互相干扰。

这已经不是简单聊天,而是后续“移动同步、子弹同步、战斗状态同步”的地基。


4. 断线自动清理

当玩家断开连接时,服务端会自动:

  • 让该玩家离开房间
  • 通知剩余房间成员
  • 在空房时自动销毁房间

这对真实联机游戏非常重要。


5. 房间状态可观测

通过 get_rooms,你已经能在调试阶段实时观察服务端房间状态。

这对于新手理解“服务端到底存了什么”非常有帮助。


🔥 8. 下一步预告

下一篇我们会进入:

第 4 步:战斗同步

这一阶段会真正开始接近游戏服务器核心:

  • 输入同步
  • 玩家移动同步
  • 服务端权威状态
  • tick / 帧循环基础模型

为什么不建议直接做“客户端位置直发”?

因为那种方式虽然简单,但很容易:

  • 抖动
  • 穿墙
  • 不一致
  • 被作弊

所以更推荐从:

输入同步(Input Sync)

开始做,这是更接近专业游戏后端的路线。


结语

第 3 步看起来只是“做房间”,但它实际上已经把你的联机坦克游戏从“能连上”推进到了“能组织对局”。

这一步完成后,整个项目就从基础设施阶段进入了真正的游戏逻辑阶段。

先把房间系统做扎实,后面的同步系统才会顺利推进。